2026-04-05 05:14:16 +08:00
|
|
|
#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>
|
2026-04-05 21:27:00 +08:00
|
|
|
#include <cmath>
|
2026-04-05 05:14:16 +08:00
|
|
|
#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;
|
2026-04-05 20:46:24 +08:00
|
|
|
constexpr float kButtonFontSize = 14.0f;
|
2026-04-05 05:14:16 +08:00
|
|
|
constexpr float kApproximateGlyphWidth = 0.56f;
|
2026-04-05 20:46:24 +08:00
|
|
|
constexpr float kHeaderTextInset = 12.0f;
|
|
|
|
|
constexpr float kHeaderTextGap = 2.0f;
|
2026-04-05 05:14:16 +08:00
|
|
|
|
|
|
|
|
struct RuntimeLayoutNode {
|
|
|
|
|
const UIDocumentNode* source = nullptr;
|
2026-04-05 21:27:00 +08:00
|
|
|
std::string stateKey = {};
|
2026-04-05 05:14:16 +08:00
|
|
|
std::vector<RuntimeLayoutNode> children = {};
|
|
|
|
|
UISize desiredSize = {};
|
2026-04-05 21:27:00 +08:00
|
|
|
UISize minimumSize = {};
|
|
|
|
|
UISize contentDesiredSize = {};
|
2026-04-05 05:14:16 +08:00
|
|
|
UIRect rect = {};
|
2026-04-05 21:27:00 +08:00
|
|
|
UIRect scrollViewportRect = {};
|
|
|
|
|
float scrollOffsetY = 0.0f;
|
|
|
|
|
bool isScrollView = false;
|
2026-04-05 05:14:16 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 20:46:24 +08:00
|
|
|
float ComputeCenteredTextTop(const UIRect& rect, float fontSize) {
|
|
|
|
|
return rect.y + (std::max)(0.0f, std::floor((rect.height - fontSize) * 0.5f));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:14:16 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 20:46:24 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:14:16 +08:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 20:46:24 +08:00
|
|
|
Layout::UILayoutItem BuildLayoutItem(
|
|
|
|
|
const RuntimeLayoutNode& child,
|
2026-04-05 21:27:00 +08:00
|
|
|
Layout::UILayoutAxis parentAxis,
|
|
|
|
|
const UISize& measuredContentSize) {
|
2026-04-05 20:46:24 +08:00
|
|
|
Layout::UILayoutItem item = {};
|
2026-04-05 21:27:00 +08:00
|
|
|
item.desiredContentSize = measuredContentSize;
|
|
|
|
|
item.minSize = child.minimumSize;
|
2026-04-05 20:46:24 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:14:16 +08:00
|
|
|
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";
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 21:27:00 +08:00
|
|
|
bool IsScrollViewTag(const std::string& tagName) {
|
|
|
|
|
return tagName == "ScrollView";
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:14:16 +08:00
|
|
|
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" ||
|
2026-04-05 21:27:00 +08:00
|
|
|
tagName == "ScrollView" ||
|
2026-04-05 05:14:16 +08:00
|
|
|
tagName == "Card" ||
|
|
|
|
|
tagName == "Button";
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 21:27:00 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:14:16 +08:00
|
|
|
Color ResolveBackgroundColor(const UIDocumentNode& node) {
|
|
|
|
|
const std::string tone = GetAttribute(node, "tone");
|
|
|
|
|
const std::string tagName = ToStdString(node.tagName);
|
|
|
|
|
|
|
|
|
|
if (tagName == "View") {
|
2026-04-05 20:46:24 +08:00
|
|
|
return Color(0.11f, 0.11f, 0.11f, 1.0f);
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
if (tone == "accent") {
|
2026-04-05 20:46:24 +08:00
|
|
|
return Color(0.25f, 0.25f, 0.25f, 1.0f);
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
if (tone == "accent-alt") {
|
2026-04-05 20:46:24 +08:00
|
|
|
return Color(0.22f, 0.22f, 0.22f, 1.0f);
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
if (tagName == "Button") {
|
2026-04-05 20:46:24 +08:00
|
|
|
return Color(0.24f, 0.24f, 0.24f, 1.0f);
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 20:46:24 +08:00
|
|
|
return Color(0.16f, 0.16f, 0.16f, 1.0f);
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Color ResolveBorderColor(const UIDocumentNode& node) {
|
|
|
|
|
const std::string tone = GetAttribute(node, "tone");
|
|
|
|
|
if (tone == "accent") {
|
2026-04-05 20:46:24 +08:00
|
|
|
return Color(0.42f, 0.42f, 0.42f, 1.0f);
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
if (tone == "accent-alt") {
|
2026-04-05 20:46:24 +08:00
|
|
|
return Color(0.34f, 0.34f, 0.34f, 1.0f);
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 20:46:24 +08:00
|
|
|
return Color(0.30f, 0.30f, 0.30f, 1.0f);
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 21:27:00 +08:00
|
|
|
RuntimeLayoutNode BuildLayoutTree(
|
|
|
|
|
const UIDocumentNode& source,
|
|
|
|
|
const std::string& parentStateKey,
|
|
|
|
|
std::size_t siblingIndex) {
|
2026-04-05 05:14:16 +08:00
|
|
|
RuntimeLayoutNode node = {};
|
|
|
|
|
node.source = &source;
|
2026-04-05 21:27:00 +08:00
|
|
|
node.stateKey = parentStateKey + "/" + BuildNodeStateKeySegment(source, siblingIndex);
|
|
|
|
|
node.isScrollView = IsScrollViewTag(ToStdString(source.tagName));
|
2026-04-05 05:14:16 +08:00
|
|
|
node.children.reserve(source.children.Size());
|
2026-04-05 21:27:00 +08:00
|
|
|
for (std::size_t index = 0; index < source.children.Size(); ++index) {
|
|
|
|
|
node.children.push_back(BuildLayoutTree(source.children[index], node.stateKey, index));
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
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));
|
2026-04-05 21:27:00 +08:00
|
|
|
node.minimumSize = node.desiredSize;
|
2026-04-05 05:14:16 +08:00
|
|
|
return node.desiredSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!IsContainerTag(source)) {
|
|
|
|
|
node.desiredSize = UISize(
|
|
|
|
|
(std::max)(160.0f, MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + 24.0f),
|
|
|
|
|
44.0f);
|
2026-04-05 21:27:00 +08:00
|
|
|
node.minimumSize = node.desiredSize;
|
2026-04-05 05:14:16 +08:00
|
|
|
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);
|
|
|
|
|
|
2026-04-05 21:27:00 +08:00
|
|
|
std::vector<Layout::UILayoutItem> desiredItems = {};
|
|
|
|
|
desiredItems.reserve(node.children.size());
|
|
|
|
|
std::vector<Layout::UILayoutItem> minimumItems = {};
|
|
|
|
|
minimumItems.reserve(node.children.size());
|
2026-04-05 05:14:16 +08:00
|
|
|
for (RuntimeLayoutNode& child : node.children) {
|
2026-04-05 20:46:24 +08:00
|
|
|
MeasureNode(child);
|
2026-04-05 21:27:00 +08:00
|
|
|
desiredItems.push_back(BuildLayoutItem(child, options.axis, child.desiredSize));
|
|
|
|
|
minimumItems.push_back(BuildLayoutItem(child, options.axis, child.minimumSize));
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 21:27:00 +08:00
|
|
|
const auto measured = Layout::MeasureStackLayout(options, desiredItems);
|
|
|
|
|
const auto minimumMeasured = Layout::MeasureStackLayout(options, minimumItems);
|
|
|
|
|
node.contentDesiredSize = measured.desiredSize;
|
2026-04-05 05:14:16 +08:00
|
|
|
node.desiredSize = measured.desiredSize;
|
2026-04-05 21:27:00 +08:00
|
|
|
node.minimumSize = minimumMeasured.desiredSize;
|
2026-04-05 05:14:16 +08:00
|
|
|
|
2026-04-05 20:46:24 +08:00
|
|
|
const float headerHeight = MeasureHeaderHeight(source);
|
|
|
|
|
const float headerTextWidth = MeasureHeaderTextWidth(source);
|
2026-04-05 05:14:16 +08:00
|
|
|
|
|
|
|
|
node.desiredSize.width = (std::max)(
|
|
|
|
|
node.desiredSize.width,
|
2026-04-05 20:46:24 +08:00
|
|
|
headerTextWidth > 0.0f
|
|
|
|
|
? headerTextWidth + options.padding.Horizontal()
|
|
|
|
|
: MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + options.padding.Horizontal());
|
2026-04-05 05:14:16 +08:00
|
|
|
node.desiredSize.height += headerHeight;
|
2026-04-05 21:27:00 +08:00
|
|
|
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();
|
|
|
|
|
}
|
2026-04-05 05:14:16 +08:00
|
|
|
|
|
|
|
|
float explicitWidth = 0.0f;
|
|
|
|
|
if (TryParseFloat(GetAttribute(source, "width"), explicitWidth)) {
|
2026-04-05 20:46:24 +08:00
|
|
|
node.desiredSize.width = (std::max)(node.desiredSize.width, explicitWidth);
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float explicitHeight = 0.0f;
|
|
|
|
|
if (TryParseFloat(GetAttribute(source, "height"), explicitHeight)) {
|
2026-04-05 20:46:24 +08:00
|
|
|
node.desiredSize.height = (std::max)(node.desiredSize.height, explicitHeight);
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return node.desiredSize;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 21:27:00 +08:00
|
|
|
void ArrangeNode(
|
|
|
|
|
RuntimeLayoutNode& node,
|
|
|
|
|
const UIRect& rect,
|
|
|
|
|
const std::unordered_map<std::string, float>& verticalScrollOffsets) {
|
2026-04-05 05:14:16 +08:00
|
|
|
node.rect = rect;
|
2026-04-05 21:27:00 +08:00
|
|
|
node.scrollViewportRect = {};
|
|
|
|
|
node.scrollOffsetY = 0.0f;
|
2026-04-05 05:14:16 +08:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
2026-04-05 20:46:24 +08:00
|
|
|
const float headerHeight = MeasureHeaderHeight(source);
|
2026-04-05 05:14:16 +08:00
|
|
|
|
|
|
|
|
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) {
|
2026-04-05 21:27:00 +08:00
|
|
|
Layout::UILayoutItem item = BuildLayoutItem(child, options.axis, child.desiredSize);
|
2026-04-05 05:14:16 +08:00
|
|
|
items.push_back(item);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 21:27:00 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:14:16 +08:00
|
|
|
const auto arranged = Layout::ArrangeStackLayout(options, items, contentRect);
|
|
|
|
|
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
2026-04-05 21:27:00 +08:00
|
|
|
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);
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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");
|
2026-04-05 20:46:24 +08:00
|
|
|
float textY = node.rect.y + kHeaderTextInset;
|
2026-04-05 05:14:16 +08:00
|
|
|
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;
|
2026-04-05 20:46:24 +08:00
|
|
|
textY += MeasureTextHeight(kDefaultFontSize) + kHeaderTextGap;
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(
|
2026-04-05 20:46:24 +08:00
|
|
|
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)),
|
2026-04-05 20:46:24 +08:00
|
|
|
kButtonFontSize);
|
2026-04-05 05:44:07 +08:00
|
|
|
++stats.textCommandCount;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 21:27:00 +08:00
|
|
|
const bool pushScrollClip =
|
|
|
|
|
node.isScrollView &&
|
|
|
|
|
node.scrollViewportRect.width > 0.0f &&
|
|
|
|
|
node.scrollViewportRect.height > 0.0f;
|
|
|
|
|
if (pushScrollClip) {
|
|
|
|
|
drawList.PushClipRect(node.scrollViewportRect);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:14:16 +08:00
|
|
|
for (const RuntimeLayoutNode& child : node.children) {
|
|
|
|
|
EmitNode(child, drawList, stats);
|
|
|
|
|
}
|
2026-04-05 21:27:00 +08:00
|
|
|
|
|
|
|
|
if (pushScrollClip) {
|
|
|
|
|
drawList.PopClipRect();
|
|
|
|
|
}
|
2026-04-05 05:14:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 21:27:00 +08:00
|
|
|
const std::string stateRoot = document.sourcePath.empty()
|
|
|
|
|
? document.displayName
|
|
|
|
|
: document.sourcePath;
|
|
|
|
|
RuntimeLayoutNode root = BuildLayoutTree(document.viewDocument.rootNode, stateRoot, 0u);
|
2026-04-05 05:14:16 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 21:27:00 +08:00
|
|
|
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;
|
|
|
|
|
}
|
2026-04-05 05:14:16 +08:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 21:27:00 +08:00
|
|
|
const UIDocumentScreenHost::ScrollDebugSnapshot& UIDocumentScreenHost::GetScrollDebugSnapshot() const {
|
|
|
|
|
return m_scrollDebugSnapshot;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:14:16 +08:00
|
|
|
} // namespace Runtime
|
|
|
|
|
} // namespace UI
|
|
|
|
|
} // namespace XCEngine
|