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

@@ -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) {