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>
|
|
|
|
|
#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;
|
|
|
|
|
std::vector<RuntimeLayoutNode> 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<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,
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
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";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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") {
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<Layout::UILayoutItem> items = {};
|
|
|
|
|
items.reserve(node.children.size());
|
|
|
|
|
for (RuntimeLayoutNode& child : node.children) {
|
2026-04-05 20:46:24 +08:00
|
|
|
MeasureNode(child);
|
|
|
|
|
Layout::UILayoutItem item = BuildLayoutItem(child, options.axis);
|
2026-04-05 05:14:16 +08:00
|
|
|
items.push_back(item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto measured = Layout::MeasureStackLayout(options, items);
|
|
|
|
|
node.desiredSize = measured.desiredSize;
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
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 20:46:24 +08:00
|
|
|
Layout::UILayoutItem item = BuildLayoutItem(child, options.axis);
|
2026-04-05 05:14:16 +08:00
|
|
|
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");
|
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 05:14:16 +08:00
|
|
|
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<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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|