#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 kButtonFontSize = 14.0f; constexpr float kApproximateGlyphWidth = 0.56f; constexpr float kHeaderTextInset = 12.0f; constexpr float kHeaderTextGap = 2.0f; 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; } float ComputeCenteredTextTop(const UIRect& rect, float fontSize) { return rect.y + (std::max)(0.0f, std::floor((rect.height - fontSize) * 0.5f)); } 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; } float MeasureHeaderTextWidth(const UIDocumentNode& node) { float width = 0.0f; const std::string title = GetAttribute(node, "title"); if (!title.empty()) { width = (std::max)(width, MeasureTextWidth(title, kDefaultFontSize)); } const std::string subtitle = GetAttribute(node, "subtitle"); if (!subtitle.empty()) { width = (std::max)(width, MeasureTextWidth(subtitle, kSmallFontSize)); } return width; } float MeasureHeaderHeight(const UIDocumentNode& node) { const std::string title = GetAttribute(node, "title"); const std::string subtitle = GetAttribute(node, "subtitle"); if (title.empty() && subtitle.empty()) { return 0.0f; } float headerHeight = kHeaderTextInset; if (!title.empty()) { headerHeight += MeasureTextHeight(kDefaultFontSize); } if (!subtitle.empty()) { if (!title.empty()) { headerHeight += kHeaderTextGap; } headerHeight += MeasureTextHeight(kSmallFontSize); } return headerHeight; } 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)); } Layout::UILayoutItem BuildLayoutItem( const RuntimeLayoutNode& child, Layout::UILayoutAxis parentAxis) { Layout::UILayoutItem item = {}; item.desiredContentSize = child.desiredSize; item.width = ParseLengthAttribute(*child.source, "width"); item.height = ParseLengthAttribute(*child.source, "height"); // Pixel-authored lengths act as requested extents, but never below the measured content floor. if (item.width.unit == Layout::UILayoutLengthUnit::Pixels && item.width.value < child.desiredSize.width) { item.minSize.width = child.desiredSize.width; } if (item.height.unit == Layout::UILayoutLengthUnit::Pixels && item.height.value < child.desiredSize.height) { item.minSize.height = child.desiredSize.height; } if (parentAxis == Layout::UILayoutAxis::Vertical && item.width.unit == Layout::UILayoutLengthUnit::Auto) { item.horizontalAlignment = Layout::UILayoutAlignment::Stretch; } if (parentAxis == Layout::UILayoutAxis::Horizontal && item.height.unit == Layout::UILayoutLengthUnit::Auto) { item.verticalAlignment = Layout::UILayoutAlignment::Stretch; } return item; } 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.11f, 0.11f, 0.11f, 1.0f); } if (tone == "accent") { return Color(0.25f, 0.25f, 0.25f, 1.0f); } if (tone == "accent-alt") { return Color(0.22f, 0.22f, 0.22f, 1.0f); } if (tagName == "Button") { return Color(0.24f, 0.24f, 0.24f, 1.0f); } return Color(0.16f, 0.16f, 0.16f, 1.0f); } Color ResolveBorderColor(const UIDocumentNode& node) { const std::string tone = GetAttribute(node, "tone"); if (tone == "accent") { return Color(0.42f, 0.42f, 0.42f, 1.0f); } if (tone == "accent-alt") { return Color(0.34f, 0.34f, 0.34f, 1.0f); } return Color(0.30f, 0.30f, 0.30f, 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) { MeasureNode(child); Layout::UILayoutItem item = BuildLayoutItem(child, options.axis); items.push_back(item); } const auto measured = Layout::MeasureStackLayout(options, items); node.desiredSize = measured.desiredSize; const float headerHeight = MeasureHeaderHeight(source); const float headerTextWidth = MeasureHeaderTextWidth(source); node.desiredSize.width = (std::max)( node.desiredSize.width, headerTextWidth > 0.0f ? headerTextWidth + options.padding.Horizontal() : MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + options.padding.Horizontal()); node.desiredSize.height += headerHeight; float explicitWidth = 0.0f; if (TryParseFloat(GetAttribute(source, "width"), explicitWidth)) { node.desiredSize.width = (std::max)(node.desiredSize.width, explicitWidth); } float explicitHeight = 0.0f; if (TryParseFloat(GetAttribute(source, "height"), explicitHeight)) { node.desiredSize.height = (std::max)(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); const float headerHeight = MeasureHeaderHeight(source); 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 = BuildLayoutItem(child, options.axis); 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 + kHeaderTextInset; 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) + kHeaderTextGap; } 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, ComputeCenteredTextTop(node.rect, kButtonFontSize)), ResolveNodeText(source), ToUIColor(Color(0.95f, 0.97f, 1.0f, 1.0f)), kButtonFontSize); ++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