feat(xcui): advance core and editor validation flow

This commit is contained in:
2026-04-06 16:20:46 +08:00
parent 33bb84f650
commit 2d030a97da
128 changed files with 9961 additions and 773 deletions

View File

@@ -1,6 +1,8 @@
#pragma once
#include <XCEngine/Resources/UI/UIDocumentTypes.h>
#include <XCEngine/UI/Style/Theme.h>
#include <XCEngine/UI/Style/StyleSet.h>
#include <XCEngine/UI/DrawData.h>
#include <cstddef>
@@ -30,7 +32,10 @@ struct UIScreenDocument {
std::vector<std::string> dependencies = {};
Resources::UIDocumentModel viewDocument = {};
Resources::UIDocumentModel themeDocument = {};
Style::UITheme runtimeTheme = {};
Style::UIStyleSheet runtimeStyleSheet = {};
bool hasThemeDocument = false;
bool hasRuntimeTheme = false;
bool IsValid() const {
return !sourcePath.empty();
@@ -39,6 +44,14 @@ struct UIScreenDocument {
const Resources::UIDocumentModel* GetThemeDocument() const {
return hasThemeDocument ? &themeDocument : nullptr;
}
const Style::UITheme* GetRuntimeTheme() const {
return hasRuntimeTheme ? &runtimeTheme : nullptr;
}
const Style::UIStyleSheet* GetRuntimeStyleSheet() const {
return hasRuntimeTheme ? &runtimeStyleSheet : nullptr;
}
};
struct UIScreenLoadResult {

View File

@@ -0,0 +1,33 @@
#pragma once
#include "StyleResolver.h"
#include <XCEngine/Resources/UI/UIDocumentTypes.h>
#include <string>
namespace XCEngine {
namespace UI {
namespace Style {
struct UIDocumentStyleCompileResult {
bool succeeded = false;
std::string errorMessage = {};
UITheme theme = {};
UIStyleSheet styleSheet = {};
};
UIDocumentStyleCompileResult CompileDocumentStyle(
const Resources::UIDocumentModel& themeDocument);
bool TryCompileDocumentStyle(
const Resources::UIDocumentModel& themeDocument,
UITheme& outTheme,
UIStyleSheet& outStyleSheet,
std::string* outErrorMessage = nullptr);
UIStyleSet BuildInlineStyle(const Resources::UIDocumentNode& node);
} // namespace Style
} // namespace UI
} // namespace XCEngine

View File

@@ -6,6 +6,7 @@
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
#include <XCEngine/UI/Layout/LayoutEngine.h>
#include <XCEngine/UI/Layout/UITabStripLayout.h>
#include <XCEngine/UI/Style/DocumentStyleCompiler.h>
#include <XCEngine/UI/Widgets/UITabStripModel.h>
#include <algorithm>
@@ -34,6 +35,7 @@ using XCEngine::Resources::UIDocumentCompileResult;
using XCEngine::Resources::UIDocumentKind;
using XCEngine::Resources::UIDocumentNode;
namespace Layout = XCEngine::UI::Layout;
namespace Style = XCEngine::UI::Style;
constexpr float kDefaultFontSize = 16.0f;
constexpr float kSmallFontSize = 13.0f;
@@ -78,6 +80,8 @@ struct RuntimeLayoutNode {
bool tabSelected = false;
bool hasShortcutBinding = false;
UIShortcutBinding shortcutBinding = {};
Style::UIStyleSet localStyle = {};
Style::UIResolvedStyle resolvedStyle = {};
enum class ShortcutScopeRoot : std::uint8_t {
None = 0,
Window,
@@ -580,6 +584,166 @@ Layout::UILayoutThickness ParsePadding(
return Layout::UILayoutThickness::Uniform(ParseFloatAttribute(node, "padding", fallback));
}
Layout::UILayoutThickness ToLayoutThickness(const Style::UIThickness& thickness) {
return Layout::UILayoutThickness(
thickness.left,
thickness.top,
thickness.right,
thickness.bottom);
}
const Style::UIStylePropertyResolution* FindResolvedProperty(
const RuntimeLayoutNode& node,
Style::UIStylePropertyId propertyId) {
return node.resolvedStyle.FindProperty(propertyId);
}
float ResolveNodeFontSize(
const RuntimeLayoutNode& node,
float fallback) {
const Style::UIStylePropertyResolution* resolution =
FindResolvedProperty(node, Style::UIStylePropertyId::FontSize);
if (resolution == nullptr) {
return fallback;
}
if (const float* value = resolution->value.TryGetFloat()) {
return (std::max)(1.0f, *value);
}
return fallback;
}
float ResolveNodeGap(
const RuntimeLayoutNode& node,
float fallback) {
const Style::UIStylePropertyResolution* resolution =
FindResolvedProperty(node, Style::UIStylePropertyId::Gap);
if (resolution == nullptr) {
return fallback;
}
if (const float* value = resolution->value.TryGetFloat()) {
return (std::max)(0.0f, *value);
}
return fallback;
}
Layout::UILayoutThickness ResolveNodePadding(
const RuntimeLayoutNode& node,
float fallback) {
const Style::UIStylePropertyResolution* resolution =
FindResolvedProperty(node, Style::UIStylePropertyId::Padding);
if (resolution == nullptr) {
return Layout::UILayoutThickness::Uniform(fallback);
}
if (const Style::UIThickness* thickness = resolution->value.TryGetThickness()) {
return ToLayoutThickness(*thickness);
}
return Layout::UILayoutThickness::Uniform(fallback);
}
float ResolveNodeBorderWidth(
const RuntimeLayoutNode& node,
float fallback) {
const Style::UIStylePropertyResolution* resolution =
FindResolvedProperty(node, Style::UIStylePropertyId::BorderWidth);
if (resolution == nullptr) {
return fallback;
}
if (const float* value = resolution->value.TryGetFloat()) {
return (std::max)(0.0f, *value);
}
return fallback;
}
float ResolveNodeCornerRadius(
const RuntimeLayoutNode& node,
float fallback) {
const Style::UIStylePropertyResolution* resolution =
FindResolvedProperty(node, Style::UIStylePropertyId::CornerRadius);
if (resolution == nullptr) {
return fallback;
}
if (const Style::UICornerRadius* radius = resolution->value.TryGetCornerRadius()) {
return (std::max)({
0.0f,
radius->topLeft,
radius->topRight,
radius->bottomRight,
radius->bottomLeft
});
}
return fallback;
}
Color AdjustColorBrightness(const Color& color, float delta) {
auto clampChannel = [delta](float value) {
return (std::clamp)(value + delta, 0.0f, 1.0f);
};
return Color(
clampChannel(color.r),
clampChannel(color.g),
clampChannel(color.b),
color.a);
}
Color ResolveLegacyBaseBackgroundColor(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 ResolveLegacyBaseBorderColor(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);
}
Color ResolveStyledColor(
const RuntimeLayoutNode& node,
Style::UIStylePropertyId propertyId,
const Color& fallback) {
const Style::UIStylePropertyResolution* resolution = FindResolvedProperty(node, propertyId);
if (resolution == nullptr) {
return fallback;
}
if (const Color* color = resolution->value.TryGetColor()) {
return *color;
}
return fallback;
}
Layout::UILayoutItem BuildLayoutItem(
const RuntimeLayoutNode& child,
Layout::UILayoutAxis parentAxis,
@@ -1283,61 +1447,68 @@ UIInputPath ResolveHoveredPath(
}
Color ResolveBackgroundColor(
const UIDocumentNode& node,
const RuntimeLayoutNode& node,
const RuntimeNodeVisualState& state) {
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);
}
const std::string tagName = ToStdString(node.source->tagName);
const Color baseColor = ResolveStyledColor(
node,
Style::UIStylePropertyId::BackgroundColor,
ResolveLegacyBaseBackgroundColor(*node.source));
if (tagName == "Button") {
if (state.active || state.capture) {
return Color(0.30f, 0.30f, 0.30f, 1.0f);
return AdjustColorBrightness(baseColor, 0.06f);
}
if (state.hovered) {
return Color(0.27f, 0.27f, 0.27f, 1.0f);
return AdjustColorBrightness(baseColor, 0.03f);
}
return Color(0.24f, 0.24f, 0.24f, 1.0f);
}
return Color(0.16f, 0.16f, 0.16f, 1.0f);
return baseColor;
}
Color ResolveBorderColor(
const UIDocumentNode& node,
const RuntimeLayoutNode& node,
const RuntimeNodeVisualState& state) {
const Color baseColor = ResolveStyledColor(
node,
Style::UIStylePropertyId::BorderColor,
ResolveLegacyBaseBorderColor(*node.source));
if (state.capture) {
return Color(0.82f, 0.82f, 0.82f, 1.0f);
return AdjustColorBrightness(baseColor, 0.40f);
}
if (state.focused || state.active) {
return Color(0.62f, 0.62f, 0.62f, 1.0f);
return AdjustColorBrightness(baseColor, 0.24f);
}
if (state.hovered) {
return Color(0.45f, 0.45f, 0.45f, 1.0f);
return AdjustColorBrightness(baseColor, 0.12f);
}
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);
return baseColor;
}
float ResolveBorderThickness(const RuntimeNodeVisualState& state) {
return (state.focused || state.active || state.capture) ? 2.0f : 1.0f;
float ResolveBorderThickness(
const RuntimeLayoutNode& node,
const RuntimeNodeVisualState& state) {
const float baseThickness = ResolveNodeBorderWidth(node, 1.0f);
if (state.focused || state.active || state.capture) {
return (std::max)(baseThickness, 2.0f);
}
return baseThickness;
}
Color ResolveForegroundColor(
const RuntimeLayoutNode& node,
const RuntimeNodeVisualState& state,
const Color& fallback) {
Color color = ResolveStyledColor(node, Style::UIStylePropertyId::ForegroundColor, fallback);
if (ToStdString(node.source->tagName) == "Button" &&
(state.capture || state.focused || state.active)) {
color = AdjustColorBrightness(color, 0.08f);
}
return color;
}
bool IsPathTarget(
@@ -1360,6 +1531,7 @@ RuntimeNodeVisualState ResolveNodeVisualState(
RuntimeLayoutNode BuildLayoutTree(
const UIDocumentNode& source,
const UIScreenDocument& document,
const std::string& parentStateKey,
const UIInputPath& parentInputPath,
std::size_t siblingIndex,
@@ -1388,10 +1560,19 @@ RuntimeLayoutNode BuildLayoutTree(
ParseRatioAttribute(source, "ratio", 0.5f));
node.shortcutScopeRoot = ParseShortcutScopeRoot(source);
node.hasShortcutBinding = TryBuildShortcutBinding(source, node.elementId, node.shortcutBinding);
node.localStyle = Style::BuildInlineStyle(source);
Style::UIStyleResolveContext styleContext = {};
styleContext.theme = document.GetRuntimeTheme();
styleContext.styleSheet = document.GetRuntimeStyleSheet();
styleContext.selector.typeName = tagName;
styleContext.selector.styleName = GetAttribute(source, "style");
styleContext.localStyle = &node.localStyle;
node.resolvedStyle = Style::ResolveStyle(styleContext);
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],
document,
node.stateKey,
node.inputPath,
index,
@@ -1417,17 +1598,22 @@ UISize MeasureNode(RuntimeLayoutNode& node) {
if (tagName == "Text") {
const std::string text = ResolveNodeText(source);
const float fontSize = ResolveNodeFontSize(node, kDefaultFontSize);
node.desiredSize = UISize(
MeasureTextWidth(text, kDefaultFontSize),
MeasureTextHeight(kDefaultFontSize));
MeasureTextWidth(text, fontSize),
MeasureTextHeight(fontSize));
node.minimumSize = node.desiredSize;
return node.desiredSize;
}
if (!IsContainerTag(source)) {
const float fontSize = ResolveNodeFontSize(
node,
tagName == "Button" ? kButtonFontSize : kDefaultFontSize);
const Layout::UILayoutThickness padding = ResolveNodePadding(node, 12.0f);
node.desiredSize = UISize(
(std::max)(160.0f, MeasureTextWidth(ResolveNodeText(source), kDefaultFontSize) + 24.0f),
44.0f);
(std::max)(160.0f, MeasureTextWidth(ResolveNodeText(source), fontSize) + padding.Horizontal()),
(std::max)(44.0f, MeasureTextHeight(fontSize) + padding.Vertical()));
node.minimumSize = node.desiredSize;
return node.desiredSize;
}
@@ -1532,12 +1718,11 @@ UISize MeasureNode(RuntimeLayoutNode& node) {
options.axis = IsHorizontalTag(tagName)
? Layout::UILayoutAxis::Horizontal
: Layout::UILayoutAxis::Vertical;
options.spacing = ParseFloatAttribute(
source,
"gap",
options.spacing = ResolveNodeGap(
node,
options.axis == Layout::UILayoutAxis::Horizontal ? 10.0f : 8.0f);
options.padding = ParsePadding(
source,
options.padding = ResolveNodePadding(
node,
tagName == "View" ? 16.0f : 12.0f);
std::vector<Layout::UILayoutItem> desiredItems = {};
@@ -1676,12 +1861,11 @@ void ArrangeNode(
options.axis = IsHorizontalTag(tagName)
? Layout::UILayoutAxis::Horizontal
: Layout::UILayoutAxis::Vertical;
options.spacing = ParseFloatAttribute(
source,
"gap",
options.spacing = ResolveNodeGap(
node,
options.axis == Layout::UILayoutAxis::Horizontal ? 10.0f : 8.0f);
options.padding = ParsePadding(
source,
options.padding = ResolveNodePadding(
node,
tagName == "View" ? 16.0f : 12.0f);
const float headerHeight = node.isTab ? 0.0f : MeasureHeaderHeight(source);
@@ -2414,15 +2598,16 @@ void EmitNode(
++stats.nodeCount;
if (tagName == "View" || tagName == "Card" || tagName == "Button") {
drawList.AddFilledRect(node.rect, ToUIColor(ResolveBackgroundColor(source, visualState)), 10.0f);
const float cornerRadius = ResolveNodeCornerRadius(node, 10.0f);
drawList.AddFilledRect(node.rect, ToUIColor(ResolveBackgroundColor(node, visualState)), cornerRadius);
++stats.filledRectCommandCount;
if (tagName != "View") {
drawList.AddRectOutline(
node.rect,
ToUIColor(ResolveBorderColor(source, visualState)),
ResolveBorderThickness(visualState),
10.0f);
ToUIColor(ResolveBorderColor(node, visualState)),
ResolveBorderThickness(node, visualState),
cornerRadius);
}
}
@@ -2539,22 +2724,27 @@ void EmitNode(
}
if (tagName == "Text") {
const float fontSize = ResolveNodeFontSize(node, kDefaultFontSize);
drawList.AddText(
UIPoint(node.rect.x, node.rect.y),
ResolveNodeText(source),
ToUIColor(Color(0.92f, 0.94f, 0.97f, 1.0f)),
kDefaultFontSize);
ToUIColor(ResolveForegroundColor(node, visualState, Color(0.92f, 0.94f, 0.97f, 1.0f))),
fontSize);
++stats.textCommandCount;
}
if (tagName == "Button" && title.empty() && subtitle.empty()) {
const float fontSize = ResolveNodeFontSize(node, kButtonFontSize);
drawList.AddText(
UIPoint(node.rect.x + 12.0f, ComputeCenteredTextTop(node.rect, kButtonFontSize)),
UIPoint(node.rect.x + 12.0f, ComputeCenteredTextTop(node.rect, fontSize)),
ResolveNodeText(source),
ToUIColor(visualState.capture || visualState.focused
? Color(1.0f, 1.0f, 1.0f, 1.0f)
: Color(0.95f, 0.97f, 1.0f, 1.0f)),
kButtonFontSize);
ToUIColor(ResolveForegroundColor(
node,
visualState,
visualState.capture || visualState.focused
? Color(1.0f, 1.0f, 1.0f, 1.0f)
: Color(0.95f, 0.97f, 1.0f, 1.0f))),
fontSize);
++stats.textCommandCount;
}
@@ -2667,6 +2857,19 @@ UIScreenLoadResult UIDocumentScreenHost::LoadScreen(const UIScreenAsset& asset)
result.document.themeDocument = themeResult.document;
result.document.hasThemeDocument = true;
std::string runtimeThemeError = {};
if (!Style::TryCompileDocumentStyle(
result.document.themeDocument,
result.document.runtimeTheme,
result.document.runtimeStyleSheet,
&runtimeThemeError)) {
result = {};
result.errorMessage = runtimeThemeError.empty()
? "Failed to compile runtime UI theme styles."
: runtimeThemeError;
return result;
}
result.document.hasRuntimeTheme = true;
if (seenDependencies.insert(asset.themePath).second) {
result.document.dependencies.push_back(asset.themePath);
}
@@ -2693,6 +2896,7 @@ UIScreenFrameResult UIDocumentScreenHost::BuildFrame(
: document.sourcePath;
RuntimeLayoutNode root = BuildLayoutTree(
document.viewDocument.rootNode,
document,
stateRoot,
UIInputPath(),
0u,

View File

@@ -0,0 +1,598 @@
#include <XCEngine/UI/Style/DocumentStyleCompiler.h>
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <string>
#include <string_view>
#include <vector>
namespace XCEngine {
namespace UI {
namespace Style {
namespace {
using XCEngine::Math::Color;
using XCEngine::Resources::UIDocumentAttribute;
using XCEngine::Resources::UIDocumentModel;
using XCEngine::Resources::UIDocumentNode;
std::string ToStdString(const Containers::String& value) {
return value.Empty() || value.CStr() == nullptr
? std::string()
: std::string(value.CStr());
}
std::string TrimAscii(std::string value) {
std::size_t start = 0u;
while (start < value.size() &&
std::isspace(static_cast<unsigned char>(value[start])) != 0) {
++start;
}
std::size_t end = value.size();
while (end > start &&
std::isspace(static_cast<unsigned char>(value[end - 1u])) != 0) {
--end;
}
return value.substr(start, end - start);
}
std::string ToLowerAscii(std::string value) {
std::transform(
value.begin(),
value.end(),
value.begin(),
[](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return value;
}
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;
}
bool TryParseFloat(const std::string& text, float& outValue) {
const std::string trimmed = TrimAscii(text);
if (trimmed.empty()) {
return false;
}
char* end = nullptr;
const float value = std::strtof(trimmed.c_str(), &end);
if (end == trimmed.c_str()) {
return false;
}
while (*end != '\0') {
if (std::isspace(static_cast<unsigned char>(*end)) == 0) {
return false;
}
++end;
}
outValue = value;
return true;
}
int HexToInt(char ch) {
if (ch >= '0' && ch <= '9') {
return ch - '0';
}
if (ch >= 'a' && ch <= 'f') {
return 10 + (ch - 'a');
}
if (ch >= 'A' && ch <= 'F') {
return 10 + (ch - 'A');
}
return -1;
}
bool TryParseHexByte(std::string_view text, std::uint8_t& outValue) {
if (text.size() != 2u) {
return false;
}
const int high = HexToInt(text[0]);
const int low = HexToInt(text[1]);
if (high < 0 || low < 0) {
return false;
}
outValue = static_cast<std::uint8_t>((high << 4) | low);
return true;
}
bool TryParseColorValue(const std::string& text, Color& outColor) {
const std::string trimmed = TrimAscii(text);
if (trimmed.size() != 7u && trimmed.size() != 9u) {
return false;
}
if (trimmed.front() != '#') {
return false;
}
std::uint8_t r = 0u;
std::uint8_t g = 0u;
std::uint8_t b = 0u;
std::uint8_t a = 255u;
if (!TryParseHexByte(std::string_view(trimmed).substr(1u, 2u), r) ||
!TryParseHexByte(std::string_view(trimmed).substr(3u, 2u), g) ||
!TryParseHexByte(std::string_view(trimmed).substr(5u, 2u), b)) {
return false;
}
if (trimmed.size() == 9u &&
!TryParseHexByte(std::string_view(trimmed).substr(7u, 2u), a)) {
return false;
}
constexpr float kInv255 = 1.0f / 255.0f;
outColor = Color(
static_cast<float>(r) * kInv255,
static_cast<float>(g) * kInv255,
static_cast<float>(b) * kInv255,
static_cast<float>(a) * kInv255);
return true;
}
bool TryParseFloatList(const std::string& text, std::vector<float>& outValues) {
outValues.clear();
std::string token = {};
auto flushToken = [&]() {
if (token.empty()) {
return true;
}
float value = 0.0f;
if (!TryParseFloat(token, value)) {
return false;
}
outValues.push_back(value);
token.clear();
return true;
};
for (char ch : text) {
if (std::isspace(static_cast<unsigned char>(ch)) != 0 ||
ch == ',' ||
ch == ';') {
if (!flushToken()) {
return false;
}
continue;
}
token.push_back(ch);
}
return flushToken() && !outValues.empty();
}
bool TryParseThicknessValue(const std::string& text, UIThickness& outThickness) {
std::vector<float> values = {};
if (!TryParseFloatList(text, values)) {
return false;
}
if (values.size() == 1u) {
outThickness = UIThickness::Uniform(values[0]);
return true;
}
if (values.size() == 2u) {
const float vertical = values[0];
const float horizontal = values[1];
outThickness = UIThickness{ horizontal, vertical, horizontal, vertical };
return true;
}
if (values.size() == 4u) {
const float top = values[0];
const float right = values[1];
const float bottom = values[2];
const float left = values[3];
outThickness = UIThickness{ left, top, right, bottom };
return true;
}
return false;
}
bool TryParseCornerRadiusValue(const std::string& text, UICornerRadius& outRadius) {
std::vector<float> values = {};
if (!TryParseFloatList(text, values)) {
return false;
}
if (values.size() == 1u) {
outRadius = UICornerRadius::Uniform(values[0]);
return true;
}
if (values.size() == 2u) {
outRadius = UICornerRadius{ values[0], values[1], values[0], values[1] };
return true;
}
if (values.size() == 4u) {
outRadius = UICornerRadius{ values[0], values[1], values[2], values[3] };
return true;
}
return false;
}
bool TryMapPropertyName(const std::string& name, UIStylePropertyId& outPropertyId) {
const std::string normalized = ToLowerAscii(TrimAscii(name));
if (normalized == "background" || normalized == "backgroundcolor") {
outPropertyId = UIStylePropertyId::BackgroundColor;
return true;
}
if (normalized == "foreground" || normalized == "foregroundcolor" || normalized == "textcolor") {
outPropertyId = UIStylePropertyId::ForegroundColor;
return true;
}
if (normalized == "bordercolor") {
outPropertyId = UIStylePropertyId::BorderColor;
return true;
}
if (normalized == "borderwidth") {
outPropertyId = UIStylePropertyId::BorderWidth;
return true;
}
if (normalized == "radius" || normalized == "cornerradius") {
outPropertyId = UIStylePropertyId::CornerRadius;
return true;
}
if (normalized == "padding") {
outPropertyId = UIStylePropertyId::Padding;
return true;
}
if (normalized == "gap" || normalized == "spacing") {
outPropertyId = UIStylePropertyId::Gap;
return true;
}
if (normalized == "fontsize") {
outPropertyId = UIStylePropertyId::FontSize;
return true;
}
if (normalized == "linewidth") {
outPropertyId = UIStylePropertyId::LineWidth;
return true;
}
return false;
}
bool TryParseTokenReferenceValue(const std::string& text, UIStyleValue& outValue) {
const std::string trimmed = TrimAscii(text);
if (trimmed.empty()) {
return false;
}
outValue = UIStyleValue::Token(trimmed);
return true;
}
bool TryParsePropertyValue(
UIStylePropertyId propertyId,
const std::string& text,
UIStyleValue& outValue) {
const std::string trimmed = TrimAscii(text);
if (trimmed.empty()) {
return false;
}
const UIStyleValueType expectedType = GetExpectedValueType(propertyId);
switch (expectedType) {
case UIStyleValueType::Color: {
Color color = {};
if (TryParseColorValue(trimmed, color)) {
outValue = UIStyleValue(color);
return true;
}
return TryParseTokenReferenceValue(trimmed, outValue);
}
case UIStyleValueType::Float: {
float value = 0.0f;
if (TryParseFloat(trimmed, value)) {
outValue = UIStyleValue(value);
return true;
}
return TryParseTokenReferenceValue(trimmed, outValue);
}
case UIStyleValueType::Thickness: {
UIThickness thickness = {};
if (TryParseThicknessValue(trimmed, thickness)) {
outValue = UIStyleValue(thickness);
return true;
}
float uniform = 0.0f;
if (TryParseFloat(trimmed, uniform)) {
outValue = UIStyleValue(uniform);
return true;
}
return TryParseTokenReferenceValue(trimmed, outValue);
}
case UIStyleValueType::CornerRadius: {
UICornerRadius radius = {};
if (TryParseCornerRadiusValue(trimmed, radius)) {
outValue = UIStyleValue(radius);
return true;
}
float uniform = 0.0f;
if (TryParseFloat(trimmed, uniform)) {
outValue = UIStyleValue(uniform);
return true;
}
return TryParseTokenReferenceValue(trimmed, outValue);
}
default:
return false;
}
}
bool TryParseThemeTokenValue(const UIDocumentNode& tokenNode, UIStyleValue& outValue) {
const std::string tokenTag = ToLowerAscii(ToStdString(tokenNode.tagName));
const std::string valueText = GetAttribute(tokenNode, "value");
if (valueText.empty()) {
return false;
}
if (tokenTag == "color") {
Color color = {};
if (TryParseColorValue(valueText, color)) {
outValue = UIStyleValue(color);
return true;
}
return TryParseTokenReferenceValue(valueText, outValue);
}
if (tokenTag == "spacing" || tokenTag == "float" || tokenTag == "number") {
float value = 0.0f;
if (TryParseFloat(valueText, value)) {
outValue = UIStyleValue(value);
return true;
}
return TryParseTokenReferenceValue(valueText, outValue);
}
if (tokenTag == "radius") {
UICornerRadius radius = {};
if (TryParseCornerRadiusValue(valueText, radius)) {
outValue = UIStyleValue(radius);
return true;
}
float uniform = 0.0f;
if (TryParseFloat(valueText, uniform)) {
outValue = UIStyleValue(uniform);
return true;
}
return TryParseTokenReferenceValue(valueText, outValue);
}
if (tokenTag == "padding" || tokenTag == "thickness") {
UIThickness thickness = {};
if (TryParseThicknessValue(valueText, thickness)) {
outValue = UIStyleValue(thickness);
return true;
}
float uniform = 0.0f;
if (TryParseFloat(valueText, uniform)) {
outValue = UIStyleValue(uniform);
return true;
}
return TryParseTokenReferenceValue(valueText, outValue);
}
return false;
}
UIStyleSet& SelectStyleSetForWidget(
const UIDocumentNode& widgetNode,
UIStyleSheet& styleSheet) {
const std::string styleName = TrimAscii(GetAttribute(widgetNode, "style"));
if (!styleName.empty()) {
if (ToLowerAscii(styleName) == "default") {
return styleSheet.DefaultStyle();
}
return styleSheet.GetOrCreateNamedStyle(styleName);
}
const std::string typeName = TrimAscii(GetAttribute(widgetNode, "type"));
if (typeName.empty()) {
return styleSheet.DefaultStyle();
}
const std::string normalized = ToLowerAscii(typeName);
if (normalized == "*" || normalized == "default") {
return styleSheet.DefaultStyle();
}
return styleSheet.GetOrCreateTypeStyle(typeName);
}
bool ParseTokensNode(
const UIDocumentNode& tokensNode,
UIThemeDefinition& outDefinition,
std::string& outErrorMessage) {
for (const UIDocumentNode& tokenNode : tokensNode.children) {
const std::string name = TrimAscii(GetAttribute(tokenNode, "name"));
if (name.empty()) {
outErrorMessage = "Theme token is missing required 'name' attribute.";
return false;
}
UIStyleValue value = {};
if (!TryParseThemeTokenValue(tokenNode, value)) {
outErrorMessage = "Theme token '" + name + "' has an unsupported value.";
return false;
}
outDefinition.SetToken(name, value);
}
return true;
}
bool ParseWidgetsNode(
const UIDocumentNode& widgetsNode,
UIStyleSheet& outStyleSheet,
std::string& outErrorMessage) {
for (const UIDocumentNode& widgetNode : widgetsNode.children) {
if (widgetNode.tagName != "Widget") {
outErrorMessage = "Theme <Widgets> only supports <Widget> children.";
return false;
}
const std::string styleName = TrimAscii(GetAttribute(widgetNode, "style"));
const std::string typeName = TrimAscii(GetAttribute(widgetNode, "type"));
if (styleName.empty() && typeName.empty()) {
outErrorMessage = "Theme <Widget> must declare either 'type' or 'style'.";
return false;
}
UIStyleSet& styleSet = SelectStyleSetForWidget(widgetNode, outStyleSheet);
for (const UIDocumentNode& propertyNode : widgetNode.children) {
if (propertyNode.tagName != "Property") {
outErrorMessage = "Theme <Widget> only supports <Property> children.";
return false;
}
UIStylePropertyId propertyId = UIStylePropertyId::BackgroundColor;
const std::string propertyName = GetAttribute(propertyNode, "name");
if (!TryMapPropertyName(propertyName, propertyId)) {
outErrorMessage = "Theme property '" + propertyName + "' is unsupported.";
return false;
}
UIStyleValue value = {};
if (!TryParsePropertyValue(propertyId, GetAttribute(propertyNode, "value"), value)) {
outErrorMessage = "Theme property '" + propertyName + "' has an unsupported value.";
return false;
}
styleSet.SetProperty(propertyId, value);
}
}
return true;
}
} // namespace
UIDocumentStyleCompileResult CompileDocumentStyle(const UIDocumentModel& themeDocument) {
UIDocumentStyleCompileResult result = {};
result.succeeded = TryCompileDocumentStyle(
themeDocument,
result.theme,
result.styleSheet,
&result.errorMessage);
return result;
}
bool TryCompileDocumentStyle(
const UIDocumentModel& themeDocument,
UITheme& outTheme,
UIStyleSheet& outStyleSheet,
std::string* outErrorMessage) {
outTheme = {};
outStyleSheet = {};
if (outErrorMessage != nullptr) {
outErrorMessage->clear();
}
if (!themeDocument.valid || themeDocument.rootNode.tagName.Empty()) {
if (outErrorMessage != nullptr) {
*outErrorMessage = "Theme document is invalid.";
}
return false;
}
if (themeDocument.rootNode.tagName != "Theme") {
if (outErrorMessage != nullptr) {
*outErrorMessage = "Theme document root tag must be <Theme>.";
}
return false;
}
UIThemeDefinition definition = {};
definition.name = GetAttribute(themeDocument.rootNode, "name");
std::string errorMessage = {};
for (const UIDocumentNode& childNode : themeDocument.rootNode.children) {
if (childNode.tagName == "Tokens") {
if (!ParseTokensNode(childNode, definition, errorMessage)) {
if (outErrorMessage != nullptr) {
*outErrorMessage = errorMessage;
}
return false;
}
continue;
}
if (childNode.tagName == "Widgets") {
if (!ParseWidgetsNode(childNode, outStyleSheet, errorMessage)) {
if (outErrorMessage != nullptr) {
*outErrorMessage = errorMessage;
}
return false;
}
}
}
outTheme = BuildTheme(definition);
return true;
}
UIStyleSet BuildInlineStyle(const UIDocumentNode& node) {
UIStyleSet localStyle = {};
for (const UIDocumentAttribute& attribute : node.attributes) {
UIStylePropertyId propertyId = UIStylePropertyId::BackgroundColor;
if (!TryMapPropertyName(ToStdString(attribute.name), propertyId)) {
continue;
}
UIStyleValue value = {};
if (!TryParsePropertyValue(propertyId, ToStdString(attribute.value), value)) {
continue;
}
localStyle.SetProperty(propertyId, value);
}
return localStyle;
}
} // namespace Style
} // namespace UI
} // namespace XCEngine

View File

@@ -6,6 +6,36 @@ namespace Style {
namespace {
bool TryCoerceResolvedValue(
const UIStyleValue& assignedValue,
UIStyleValueType expectedType,
UIStyleValue& outValue) {
if (!assignedValue.IsSet()) {
return false;
}
if (expectedType == UIStyleValueType::None || assignedValue.GetType() == expectedType) {
outValue = assignedValue;
return true;
}
if (expectedType == UIStyleValueType::Thickness) {
if (const float* uniform = assignedValue.TryGetFloat()) {
outValue = UIStyleValue(UIThickness::Uniform(*uniform));
return true;
}
}
if (expectedType == UIStyleValueType::CornerRadius) {
if (const float* uniform = assignedValue.TryGetFloat()) {
outValue = UIStyleValue(UICornerRadius::Uniform(*uniform));
return true;
}
}
return false;
}
bool TryResolveAssignedValue(
const UIStyleValue& assignedValue,
UIStyleValueType expectedType,
@@ -20,21 +50,15 @@ bool TryResolveAssignedValue(
return false;
}
const UITokenResolveResult tokenResult = theme->ResolveToken(tokenReference->name, expectedType);
const UITokenResolveResult tokenResult = theme->ResolveToken(tokenReference->name, UIStyleValueType::None);
if (tokenResult.status != UITokenResolveStatus::Resolved) {
return false;
}
outValue = tokenResult.value;
return true;
return TryCoerceResolvedValue(tokenResult.value, expectedType, outValue);
}
if (expectedType != UIStyleValueType::None && assignedValue.GetType() != expectedType) {
return false;
}
outValue = assignedValue;
return true;
return TryCoerceResolvedValue(assignedValue, expectedType, outValue);
}
const UIStyleSet* GetStyleSetForLayer(UIStyleLayer layer, const UIStyleResolveContext& context) {