#include #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::string stateKey = {}; std::vector children = {}; UISize desiredSize = {}; UISize minimumSize = {}; UISize contentDesiredSize = {}; UIRect rect = {}; UIRect scrollViewportRect = {}; float scrollOffsetY = 0.0f; bool isScrollView = false; }; 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, const UISize& measuredContentSize) { Layout::UILayoutItem item = {}; item.desiredContentSize = measuredContentSize; item.minSize = child.minimumSize; item.width = ParseLengthAttribute(*child.source, "width"); item.height = ParseLengthAttribute(*child.source, "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 IsScrollViewTag(const std::string& tagName) { return tagName == "ScrollView"; } 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 == "ScrollView" || tagName == "Card" || tagName == "Button"; } std::string BuildNodeStateKeySegment( const UIDocumentNode& source, std::size_t siblingIndex) { const std::string id = GetAttribute(source, "id"); if (!id.empty()) { return id; } const std::string name = GetAttribute(source, "name"); if (!name.empty()) { return ToStdString(source.tagName) + ":" + name; } return ToStdString(source.tagName) + "#" + std::to_string(siblingIndex); } float ComputeScrollOverflow(float contentExtent, float viewportExtent) { return (std::max)(0.0f, contentExtent - viewportExtent); } float ClampScrollOffset(float offset, float contentExtent, float viewportExtent) { return (std::max)(0.0f, (std::min)(offset, ComputeScrollOverflow(contentExtent, viewportExtent))); } bool RectContainsPoint(const UIRect& rect, const UIPoint& point) { return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height; } 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, const std::string& parentStateKey, std::size_t siblingIndex) { RuntimeLayoutNode node = {}; node.source = &source; node.stateKey = parentStateKey + "/" + BuildNodeStateKeySegment(source, siblingIndex); node.isScrollView = IsScrollViewTag(ToStdString(source.tagName)); node.children.reserve(source.children.Size()); for (std::size_t index = 0; index < source.children.Size(); ++index) { node.children.push_back(BuildLayoutTree(source.children[index], node.stateKey, index)); } 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)); node.minimumSize = node.desiredSize; return node.desiredSize; } if (!IsContainerTag(source)) { node.desiredSize = UISize( (std::max)(160.0f, MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + 24.0f), 44.0f); node.minimumSize = node.desiredSize; 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 desiredItems = {}; desiredItems.reserve(node.children.size()); std::vector minimumItems = {}; minimumItems.reserve(node.children.size()); for (RuntimeLayoutNode& child : node.children) { MeasureNode(child); desiredItems.push_back(BuildLayoutItem(child, options.axis, child.desiredSize)); minimumItems.push_back(BuildLayoutItem(child, options.axis, child.minimumSize)); } const auto measured = Layout::MeasureStackLayout(options, desiredItems); const auto minimumMeasured = Layout::MeasureStackLayout(options, minimumItems); node.contentDesiredSize = measured.desiredSize; node.desiredSize = measured.desiredSize; node.minimumSize = minimumMeasured.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; node.minimumSize.width = (std::max)( node.minimumSize.width, headerTextWidth > 0.0f ? headerTextWidth + options.padding.Horizontal() : MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + options.padding.Horizontal()); node.minimumSize.height += headerHeight; if (node.isScrollView) { node.minimumSize.height = headerHeight + options.padding.Vertical(); } 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, const std::unordered_map& verticalScrollOffsets) { node.rect = rect; node.scrollViewportRect = {}; node.scrollOffsetY = 0.0f; 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, child.desiredSize); items.push_back(item); } if (node.isScrollView) { const auto found = verticalScrollOffsets.find(node.stateKey); const float requestedOffset = found != verticalScrollOffsets.end() ? found->second : 0.0f; node.scrollViewportRect = contentRect; node.scrollOffsetY = ClampScrollOffset( requestedOffset, node.contentDesiredSize.height, contentRect.height); UIRect scrolledContentRect = contentRect; scrolledContentRect.y -= node.scrollOffsetY; const auto arranged = Layout::ArrangeStackLayout(options, items, scrolledContentRect); for (std::size_t index = 0; index < node.children.size(); ++index) { ArrangeNode( node.children[index], arranged.children[index].arrangedRect, verticalScrollOffsets); } return; } 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, verticalScrollOffsets); } } RuntimeLayoutNode* FindDeepestScrollTarget( RuntimeLayoutNode& node, const UIPoint& point) { for (RuntimeLayoutNode& child : node.children) { if (RuntimeLayoutNode* target = FindDeepestScrollTarget(child, point); target != nullptr) { return target; } } if (!node.isScrollView) { return nullptr; } if (!RectContainsPoint(node.scrollViewportRect, point)) { return nullptr; } if (ComputeScrollOverflow(node.contentDesiredSize.height, node.scrollViewportRect.height) <= 0.0f) { return nullptr; } return &node; } RuntimeLayoutNode* FindDeepestHoveredScrollView( RuntimeLayoutNode& node, const UIPoint& point) { for (RuntimeLayoutNode& child : node.children) { if (RuntimeLayoutNode* hovered = FindDeepestHoveredScrollView(child, point); hovered != nullptr) { return hovered; } } if (!node.isScrollView) { return nullptr; } return RectContainsPoint(node.scrollViewportRect, point) ? &node : nullptr; } const RuntimeLayoutNode* FindFirstScrollView(const RuntimeLayoutNode& node) { if (node.isScrollView) { return &node; } for (const RuntimeLayoutNode& child : node.children) { if (const RuntimeLayoutNode* found = FindFirstScrollView(child); found != nullptr) { return found; } } return nullptr; } bool ApplyScrollWheelInput( RuntimeLayoutNode& root, const UIScreenFrameInput& input, std::unordered_map& verticalScrollOffsets, UIDocumentScreenHost::ScrollDebugSnapshot& scrollDebugSnapshot) { bool changed = false; for (const UIInputEvent& event : input.events) { if (event.type != UIInputEventType::PointerWheel) { continue; } ++scrollDebugSnapshot.totalWheelEventCount; scrollDebugSnapshot.lastPointerPosition = event.position; scrollDebugSnapshot.lastWheelDelta = event.wheelDelta; scrollDebugSnapshot.lastTargetStateKey.clear(); scrollDebugSnapshot.lastViewportRect = {}; scrollDebugSnapshot.lastOverflow = 0.0f; scrollDebugSnapshot.lastOffsetBefore = 0.0f; scrollDebugSnapshot.lastOffsetAfter = 0.0f; scrollDebugSnapshot.lastResult = "No hovered ScrollView"; RuntimeLayoutNode* hoveredScrollView = FindDeepestHoveredScrollView(root, event.position); if (hoveredScrollView == nullptr) { continue; } scrollDebugSnapshot.lastTargetStateKey = hoveredScrollView->stateKey; scrollDebugSnapshot.lastViewportRect = hoveredScrollView->scrollViewportRect; scrollDebugSnapshot.lastOverflow = ComputeScrollOverflow( hoveredScrollView->contentDesiredSize.height, hoveredScrollView->scrollViewportRect.height); if (scrollDebugSnapshot.lastOverflow <= 0.0f) { scrollDebugSnapshot.lastResult = "Hovered ScrollView has no overflow"; continue; } RuntimeLayoutNode* target = FindDeepestScrollTarget(root, event.position); if (target == nullptr) { scrollDebugSnapshot.lastResult = "Scroll target resolution failed"; continue; } const auto found = verticalScrollOffsets.find(target->stateKey); const float oldOffset = found != verticalScrollOffsets.end() ? found->second : target->scrollOffsetY; scrollDebugSnapshot.lastOffsetBefore = oldOffset; const float scrollUnits = event.wheelDelta / 120.0f; const float nextOffset = ClampScrollOffset( oldOffset - scrollUnits * 48.0f, target->contentDesiredSize.height, target->scrollViewportRect.height); scrollDebugSnapshot.lastOffsetAfter = nextOffset; if (std::fabs(nextOffset - oldOffset) <= 0.01f) { scrollDebugSnapshot.lastResult = "Scroll delta clamped to current offset"; continue; } verticalScrollOffsets[target->stateKey] = nextOffset; ++scrollDebugSnapshot.handledWheelEventCount; scrollDebugSnapshot.lastResult = "Handled"; changed = true; } return changed; } void SyncScrollOffsets( const RuntimeLayoutNode& node, std::unordered_map& verticalScrollOffsets) { if (node.isScrollView) { const float overflow = ComputeScrollOverflow(node.contentDesiredSize.height, node.scrollViewportRect.height); if (overflow <= 0.0f || node.scrollOffsetY <= 0.0f) { verticalScrollOffsets.erase(node.stateKey); } else { verticalScrollOffsets[node.stateKey] = node.scrollOffsetY; } } for (const RuntimeLayoutNode& child : node.children) { SyncScrollOffsets(child, verticalScrollOffsets); } } 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; } const bool pushScrollClip = node.isScrollView && node.scrollViewportRect.width > 0.0f && node.scrollViewportRect.height > 0.0f; if (pushScrollClip) { drawList.PushClipRect(node.scrollViewportRect); } for (const RuntimeLayoutNode& child : node.children) { EmitNode(child, drawList, stats); } if (pushScrollClip) { drawList.PopClipRect(); } } 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; } const std::string stateRoot = document.sourcePath.empty() ? document.displayName : document.sourcePath; RuntimeLayoutNode root = BuildLayoutTree(document.viewDocument.rootNode, stateRoot, 0u); 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, m_verticalScrollOffsets); if (ApplyScrollWheelInput(root, input, m_verticalScrollOffsets, m_scrollDebugSnapshot)) { ArrangeNode(root, viewportRect, m_verticalScrollOffsets); } SyncScrollOffsets(root, m_verticalScrollOffsets); if (const RuntimeLayoutNode* primaryScrollView = FindFirstScrollView(root); primaryScrollView != nullptr) { m_scrollDebugSnapshot.primaryTargetStateKey = primaryScrollView->stateKey; m_scrollDebugSnapshot.primaryViewportRect = primaryScrollView->scrollViewportRect; m_scrollDebugSnapshot.primaryOverflow = ComputeScrollOverflow( primaryScrollView->contentDesiredSize.height, primaryScrollView->scrollViewportRect.height); } else { m_scrollDebugSnapshot.primaryTargetStateKey.clear(); m_scrollDebugSnapshot.primaryViewportRect = {}; m_scrollDebugSnapshot.primaryOverflow = 0.0f; } 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; } const UIDocumentScreenHost::ScrollDebugSnapshot& UIDocumentScreenHost::GetScrollDebugSnapshot() const { return m_scrollDebugSnapshot; } } // namespace Runtime } // namespace UI } // namespace XCEngine