Files
XCEngine/engine/src/UI/Runtime/UIScreenDocumentHost.cpp

858 lines
28 KiB
C++
Raw Normal View History

#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
#include <XCEngine/Core/Containers/String.h>
#include <XCEngine/Core/Math/Color.h>
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
#include <XCEngine/UI/Layout/LayoutEngine.h>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
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<RuntimeLayoutNode> 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<float>(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<unsigned char>(*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<Layout::UILayoutItem> desiredItems = {};
desiredItems.reserve(node.children.size());
std::vector<Layout::UILayoutItem> 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<std::string, float>& 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<Layout::UILayoutItem> 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<std::string, float>& 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<std::string, float>& 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;
}
2026-04-05 05:44:07 +08:00
if (tagName == "Button" && title.empty() && subtitle.empty()) {
drawList.AddText(
UIPoint(node.rect.x + 12.0f, ComputeCenteredTextTop(node.rect, kButtonFontSize)),
2026-04-05 05:44:07 +08:00
ResolveNodeText(source),
ToUIColor(Color(0.95f, 0.97f, 1.0f, 1.0f)),
kButtonFontSize);
2026-04-05 05:44:07 +08:00
++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<Containers::String>& dependencies,
std::unordered_set<std::string>& seenDependencies,
std::vector<std::string>& 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<std::string> 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