Add XCUI style theme token system
This commit is contained in:
139
engine/src/UI/Style/StyleResolver.cpp
Normal file
139
engine/src/UI/Style/StyleResolver.cpp
Normal file
@@ -0,0 +1,139 @@
|
||||
#include <XCEngine/UI/Style/StyleResolver.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Style {
|
||||
|
||||
namespace {
|
||||
|
||||
bool TryResolveAssignedValue(
|
||||
const UIStyleValue& assignedValue,
|
||||
UIStyleValueType expectedType,
|
||||
const UITheme* theme,
|
||||
UIStyleValue& outValue) {
|
||||
if (!assignedValue.IsSet()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (const UITokenReference* tokenReference = assignedValue.TryGetTokenReference()) {
|
||||
if (theme == nullptr || !tokenReference->IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UITokenResolveResult tokenResult = theme->ResolveToken(tokenReference->name, expectedType);
|
||||
if (tokenResult.status != UITokenResolveStatus::Resolved) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outValue = tokenResult.value;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (expectedType != UIStyleValueType::None && assignedValue.GetType() != expectedType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outValue = assignedValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
const UIStyleSet* GetStyleSetForLayer(UIStyleLayer layer, const UIStyleResolveContext& context) {
|
||||
if (context.styleSheet == nullptr) {
|
||||
return layer == UIStyleLayer::Local ? context.localStyle : nullptr;
|
||||
}
|
||||
|
||||
switch (layer) {
|
||||
case UIStyleLayer::Default:
|
||||
return &context.styleSheet->DefaultStyle();
|
||||
case UIStyleLayer::Type:
|
||||
return context.styleSheet->FindTypeStyle(context.selector.typeName);
|
||||
case UIStyleLayer::Named:
|
||||
return context.styleSheet->FindNamedStyle(context.selector.styleName);
|
||||
case UIStyleLayer::Local:
|
||||
return context.localStyle;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIStylePropertyResolution ResolveStyleProperty(
|
||||
UIStylePropertyId propertyId,
|
||||
const UIStyleResolveContext& context) {
|
||||
UIStylePropertyResolution resolution = {};
|
||||
const UIStyleValueType expectedType = GetExpectedValueType(propertyId);
|
||||
const UIStyleLayer lookupOrder[] = {
|
||||
UIStyleLayer::Local,
|
||||
UIStyleLayer::Named,
|
||||
UIStyleLayer::Type,
|
||||
UIStyleLayer::Default
|
||||
};
|
||||
|
||||
for (const UIStyleLayer layer : lookupOrder) {
|
||||
const UIStyleSet* styleSet = GetStyleSetForLayer(layer, context);
|
||||
if (styleSet == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const UIStyleValue* assignedValue = styleSet->FindProperty(propertyId);
|
||||
if (assignedValue == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UIStyleValue resolvedValue = {};
|
||||
if (!TryResolveAssignedValue(*assignedValue, expectedType, context.theme, resolvedValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
resolution.resolved = true;
|
||||
resolution.layer = layer;
|
||||
resolution.value = resolvedValue;
|
||||
return resolution;
|
||||
}
|
||||
|
||||
return resolution;
|
||||
}
|
||||
|
||||
UIResolvedStyle ResolveStyle(const UIStyleResolveContext& context) {
|
||||
UIResolvedStyle resolvedStyle = {};
|
||||
const UIStylePropertyId knownProperties[] = {
|
||||
UIStylePropertyId::BackgroundColor,
|
||||
UIStylePropertyId::ForegroundColor,
|
||||
UIStylePropertyId::BorderColor,
|
||||
UIStylePropertyId::BorderWidth,
|
||||
UIStylePropertyId::CornerRadius,
|
||||
UIStylePropertyId::Padding,
|
||||
UIStylePropertyId::Gap,
|
||||
UIStylePropertyId::FontSize,
|
||||
UIStylePropertyId::LineWidth
|
||||
};
|
||||
|
||||
for (const UIStylePropertyId propertyId : knownProperties) {
|
||||
const UIStylePropertyResolution propertyResolution = ResolveStyleProperty(propertyId, context);
|
||||
if (propertyResolution.resolved) {
|
||||
resolvedStyle.SetProperty(propertyId, propertyResolution);
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedStyle;
|
||||
}
|
||||
|
||||
const char* ToString(UIStyleLayer layer) {
|
||||
switch (layer) {
|
||||
case UIStyleLayer::Default:
|
||||
return "Default";
|
||||
case UIStyleLayer::Type:
|
||||
return "Type";
|
||||
case UIStyleLayer::Named:
|
||||
return "Named";
|
||||
case UIStyleLayer::Local:
|
||||
return "Local";
|
||||
default:
|
||||
return "None";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Style
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
245
engine/src/UI/Style/StyleTypes.cpp
Normal file
245
engine/src/UI/Style/StyleTypes.cpp
Normal file
@@ -0,0 +1,245 @@
|
||||
#include <XCEngine/UI/Style/StyleTypes.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Style {
|
||||
|
||||
namespace {
|
||||
|
||||
bool NearlyEqual(float left, float right) {
|
||||
return std::abs(left - right) <= Math::EPSILON;
|
||||
}
|
||||
|
||||
bool ColorsEqual(const Math::Color& left, const Math::Color& right) {
|
||||
return NearlyEqual(left.r, right.r) &&
|
||||
NearlyEqual(left.g, right.g) &&
|
||||
NearlyEqual(left.b, right.b) &&
|
||||
NearlyEqual(left.a, right.a);
|
||||
}
|
||||
|
||||
bool ThicknessEqual(const UIThickness& left, const UIThickness& right) {
|
||||
return NearlyEqual(left.left, right.left) &&
|
||||
NearlyEqual(left.top, right.top) &&
|
||||
NearlyEqual(left.right, right.right) &&
|
||||
NearlyEqual(left.bottom, right.bottom);
|
||||
}
|
||||
|
||||
bool CornerRadiusEqual(const UICornerRadius& left, const UICornerRadius& right) {
|
||||
return NearlyEqual(left.topLeft, right.topLeft) &&
|
||||
NearlyEqual(left.topRight, right.topRight) &&
|
||||
NearlyEqual(left.bottomRight, right.bottomRight) &&
|
||||
NearlyEqual(left.bottomLeft, right.bottomLeft);
|
||||
}
|
||||
|
||||
bool SizeEqual(const UISize& left, const UISize& right) {
|
||||
return NearlyEqual(left.width, right.width) &&
|
||||
NearlyEqual(left.height, right.height);
|
||||
}
|
||||
|
||||
bool PointEqual(const UIPoint& left, const UIPoint& right) {
|
||||
return NearlyEqual(left.x, right.x) &&
|
||||
NearlyEqual(left.y, right.y);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIThickness UIThickness::Uniform(float value) {
|
||||
return { value, value, value, value };
|
||||
}
|
||||
|
||||
bool UIThickness::IsUniform() const {
|
||||
return NearlyEqual(left, top) &&
|
||||
NearlyEqual(left, right) &&
|
||||
NearlyEqual(left, bottom);
|
||||
}
|
||||
|
||||
UICornerRadius UICornerRadius::Uniform(float value) {
|
||||
return { value, value, value, value };
|
||||
}
|
||||
|
||||
bool UICornerRadius::IsUniform() const {
|
||||
return NearlyEqual(topLeft, topRight) &&
|
||||
NearlyEqual(topLeft, bottomRight) &&
|
||||
NearlyEqual(topLeft, bottomLeft);
|
||||
}
|
||||
|
||||
bool UITokenReference::IsValid() const {
|
||||
return !name.empty();
|
||||
}
|
||||
|
||||
UIStyleValue::UIStyleValue(float value)
|
||||
: m_storage(value) {
|
||||
}
|
||||
|
||||
UIStyleValue::UIStyleValue(const Math::Color& value)
|
||||
: m_storage(value) {
|
||||
}
|
||||
|
||||
UIStyleValue::UIStyleValue(const UIThickness& value)
|
||||
: m_storage(value) {
|
||||
}
|
||||
|
||||
UIStyleValue::UIStyleValue(const UICornerRadius& value)
|
||||
: m_storage(value) {
|
||||
}
|
||||
|
||||
UIStyleValue::UIStyleValue(const UISize& value)
|
||||
: m_storage(value) {
|
||||
}
|
||||
|
||||
UIStyleValue::UIStyleValue(const UIPoint& value)
|
||||
: m_storage(value) {
|
||||
}
|
||||
|
||||
UIStyleValue::UIStyleValue(const UITokenReference& tokenReference)
|
||||
: m_storage(tokenReference) {
|
||||
}
|
||||
|
||||
UIStyleValue UIStyleValue::Token(const std::string& tokenName) {
|
||||
return UIStyleValue(UITokenReference{ tokenName });
|
||||
}
|
||||
|
||||
bool UIStyleValue::IsSet() const {
|
||||
return !std::holds_alternative<std::monostate>(m_storage);
|
||||
}
|
||||
|
||||
bool UIStyleValue::IsTokenReference() const {
|
||||
return std::holds_alternative<UITokenReference>(m_storage);
|
||||
}
|
||||
|
||||
UIStyleValueType UIStyleValue::GetType() const {
|
||||
if (std::holds_alternative<float>(m_storage)) {
|
||||
return UIStyleValueType::Float;
|
||||
}
|
||||
if (std::holds_alternative<Math::Color>(m_storage)) {
|
||||
return UIStyleValueType::Color;
|
||||
}
|
||||
if (std::holds_alternative<UIThickness>(m_storage)) {
|
||||
return UIStyleValueType::Thickness;
|
||||
}
|
||||
if (std::holds_alternative<UICornerRadius>(m_storage)) {
|
||||
return UIStyleValueType::CornerRadius;
|
||||
}
|
||||
if (std::holds_alternative<UISize>(m_storage)) {
|
||||
return UIStyleValueType::Size;
|
||||
}
|
||||
if (std::holds_alternative<UIPoint>(m_storage)) {
|
||||
return UIStyleValueType::Point;
|
||||
}
|
||||
if (std::holds_alternative<UITokenReference>(m_storage)) {
|
||||
return UIStyleValueType::TokenReference;
|
||||
}
|
||||
return UIStyleValueType::None;
|
||||
}
|
||||
|
||||
const float* UIStyleValue::TryGetFloat() const {
|
||||
return std::get_if<float>(&m_storage);
|
||||
}
|
||||
|
||||
const Math::Color* UIStyleValue::TryGetColor() const {
|
||||
return std::get_if<Math::Color>(&m_storage);
|
||||
}
|
||||
|
||||
const UIThickness* UIStyleValue::TryGetThickness() const {
|
||||
return std::get_if<UIThickness>(&m_storage);
|
||||
}
|
||||
|
||||
const UICornerRadius* UIStyleValue::TryGetCornerRadius() const {
|
||||
return std::get_if<UICornerRadius>(&m_storage);
|
||||
}
|
||||
|
||||
const UISize* UIStyleValue::TryGetSize() const {
|
||||
return std::get_if<UISize>(&m_storage);
|
||||
}
|
||||
|
||||
const UIPoint* UIStyleValue::TryGetPoint() const {
|
||||
return std::get_if<UIPoint>(&m_storage);
|
||||
}
|
||||
|
||||
const UITokenReference* UIStyleValue::TryGetTokenReference() const {
|
||||
return std::get_if<UITokenReference>(&m_storage);
|
||||
}
|
||||
|
||||
bool UIStyleValue::operator==(const UIStyleValue& other) const {
|
||||
if (GetType() != other.GetType()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (const float* left = TryGetFloat()) {
|
||||
return NearlyEqual(*left, *other.TryGetFloat());
|
||||
}
|
||||
if (const Math::Color* left = TryGetColor()) {
|
||||
return ColorsEqual(*left, *other.TryGetColor());
|
||||
}
|
||||
if (const UIThickness* left = TryGetThickness()) {
|
||||
return ThicknessEqual(*left, *other.TryGetThickness());
|
||||
}
|
||||
if (const UICornerRadius* left = TryGetCornerRadius()) {
|
||||
return CornerRadiusEqual(*left, *other.TryGetCornerRadius());
|
||||
}
|
||||
if (const UISize* left = TryGetSize()) {
|
||||
return SizeEqual(*left, *other.TryGetSize());
|
||||
}
|
||||
if (const UIPoint* left = TryGetPoint()) {
|
||||
return PointEqual(*left, *other.TryGetPoint());
|
||||
}
|
||||
if (const UITokenReference* left = TryGetTokenReference()) {
|
||||
return left->name == other.TryGetTokenReference()->name;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UIStyleValue::operator!=(const UIStyleValue& other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
UIStyleValueType GetExpectedValueType(UIStylePropertyId propertyId) {
|
||||
switch (propertyId) {
|
||||
case UIStylePropertyId::BackgroundColor:
|
||||
case UIStylePropertyId::ForegroundColor:
|
||||
case UIStylePropertyId::BorderColor:
|
||||
return UIStyleValueType::Color;
|
||||
case UIStylePropertyId::BorderWidth:
|
||||
case UIStylePropertyId::Gap:
|
||||
case UIStylePropertyId::FontSize:
|
||||
case UIStylePropertyId::LineWidth:
|
||||
return UIStyleValueType::Float;
|
||||
case UIStylePropertyId::CornerRadius:
|
||||
return UIStyleValueType::CornerRadius;
|
||||
case UIStylePropertyId::Padding:
|
||||
return UIStyleValueType::Thickness;
|
||||
default:
|
||||
return UIStyleValueType::None;
|
||||
}
|
||||
}
|
||||
|
||||
const char* ToString(UIStylePropertyId propertyId) {
|
||||
switch (propertyId) {
|
||||
case UIStylePropertyId::BackgroundColor:
|
||||
return "BackgroundColor";
|
||||
case UIStylePropertyId::ForegroundColor:
|
||||
return "ForegroundColor";
|
||||
case UIStylePropertyId::BorderColor:
|
||||
return "BorderColor";
|
||||
case UIStylePropertyId::BorderWidth:
|
||||
return "BorderWidth";
|
||||
case UIStylePropertyId::CornerRadius:
|
||||
return "CornerRadius";
|
||||
case UIStylePropertyId::Padding:
|
||||
return "Padding";
|
||||
case UIStylePropertyId::Gap:
|
||||
return "Gap";
|
||||
case UIStylePropertyId::FontSize:
|
||||
return "FontSize";
|
||||
case UIStylePropertyId::LineWidth:
|
||||
return "LineWidth";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Style
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
145
engine/src/UI/Style/Theme.cpp
Normal file
145
engine/src/UI/Style/Theme.cpp
Normal file
@@ -0,0 +1,145 @@
|
||||
#include <XCEngine/UI/Style/Theme.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace UI {
|
||||
namespace Style {
|
||||
|
||||
void UIThemeDefinition::SetToken(const std::string& tokenName, const UIStyleValue& value) {
|
||||
tokens[tokenName] = value;
|
||||
}
|
||||
|
||||
const UIStyleValue* UIThemeDefinition::FindToken(const std::string& tokenName) const {
|
||||
const auto it = tokens.find(tokenName);
|
||||
return it != tokens.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
UITheme::UITheme(const UIThemeDefinition& definition)
|
||||
: m_name(definition.name)
|
||||
, m_tokens(definition.tokens) {
|
||||
}
|
||||
|
||||
void UITheme::SetName(const std::string& name) {
|
||||
m_name = name;
|
||||
}
|
||||
|
||||
const std::string& UITheme::GetName() const {
|
||||
return m_name;
|
||||
}
|
||||
|
||||
void UITheme::SetParent(const UITheme* parent) {
|
||||
m_parent = parent;
|
||||
}
|
||||
|
||||
const UITheme* UITheme::GetParent() const {
|
||||
return m_parent;
|
||||
}
|
||||
|
||||
void UITheme::SetToken(const std::string& tokenName, const UIStyleValue& value) {
|
||||
m_tokens[tokenName] = value;
|
||||
}
|
||||
|
||||
bool UITheme::RemoveToken(const std::string& tokenName) {
|
||||
return m_tokens.erase(tokenName) > 0u;
|
||||
}
|
||||
|
||||
const UIStyleValue* UITheme::FindToken(const std::string& tokenName) const {
|
||||
const auto it = m_tokens.find(tokenName);
|
||||
if (it != m_tokens.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
return m_parent != nullptr ? m_parent->FindToken(tokenName) : nullptr;
|
||||
}
|
||||
|
||||
const std::map<std::string, UIStyleValue>& UITheme::GetTokens() const {
|
||||
return m_tokens;
|
||||
}
|
||||
|
||||
UITokenResolveResult UITheme::ResolveToken(
|
||||
const std::string& tokenName,
|
||||
UIStyleValueType expectedType) const {
|
||||
std::vector<std::string> visiting = {};
|
||||
return ResolveTokenRecursive(tokenName, expectedType, visiting);
|
||||
}
|
||||
|
||||
UITokenResolveResult UITheme::ResolveTokenRecursive(
|
||||
const std::string& tokenName,
|
||||
UIStyleValueType expectedType,
|
||||
std::vector<std::string>& visiting) const {
|
||||
UITokenResolveResult result = {};
|
||||
if (tokenName.empty()) {
|
||||
result.status = UITokenResolveStatus::MissingToken;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (std::find(visiting.begin(), visiting.end(), tokenName) != visiting.end()) {
|
||||
result.status = UITokenResolveStatus::CycleDetected;
|
||||
return result;
|
||||
}
|
||||
|
||||
const UIStyleValue* value = FindToken(tokenName);
|
||||
if (value == nullptr || !value->IsSet()) {
|
||||
result.status = UITokenResolveStatus::MissingToken;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (const UITokenReference* tokenReference = value->TryGetTokenReference()) {
|
||||
visiting.push_back(tokenName);
|
||||
result = ResolveTokenRecursive(tokenReference->name, expectedType, visiting);
|
||||
visiting.pop_back();
|
||||
return result;
|
||||
}
|
||||
|
||||
if (expectedType != UIStyleValueType::None && value->GetType() != expectedType) {
|
||||
result.status = UITokenResolveStatus::TypeMismatch;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.status = UITokenResolveStatus::Resolved;
|
||||
result.value = *value;
|
||||
return result;
|
||||
}
|
||||
|
||||
UITheme BuildTheme(const UIThemeDefinition& definition, const UITheme* parent) {
|
||||
UITheme theme(definition);
|
||||
theme.SetParent(parent);
|
||||
return theme;
|
||||
}
|
||||
|
||||
UITheme BuildBuiltinTheme(UIBuiltinThemeKind themeKind) {
|
||||
UIThemeDefinition definition = {};
|
||||
|
||||
if (themeKind == UIBuiltinThemeKind::NeutralLight) {
|
||||
definition.name = "NeutralLight";
|
||||
definition.SetToken("color.surface", UIStyleValue(Math::Color(0.93f, 0.94f, 0.96f, 1.0f)));
|
||||
definition.SetToken("color.surface.elevated", UIStyleValue(Math::Color(1.0f, 1.0f, 1.0f, 1.0f)));
|
||||
definition.SetToken("color.text.primary", UIStyleValue(Math::Color(0.10f, 0.11f, 0.14f, 1.0f)));
|
||||
definition.SetToken("color.accent", UIStyleValue(Math::Color(0.14f, 0.43f, 0.88f, 1.0f)));
|
||||
definition.SetToken("space.compact", UIStyleValue(6.0f));
|
||||
definition.SetToken("space.regular", UIStyleValue(10.0f));
|
||||
definition.SetToken("padding.control", UIStyleValue(UIThickness::Uniform(10.0f)));
|
||||
definition.SetToken("radius.control", UIStyleValue(UICornerRadius::Uniform(6.0f)));
|
||||
definition.SetToken("font.body", UIStyleValue(14.0f));
|
||||
definition.SetToken("line.default", UIStyleValue(1.0f));
|
||||
return BuildTheme(definition);
|
||||
}
|
||||
|
||||
definition.name = "NeutralDark";
|
||||
definition.SetToken("color.surface", UIStyleValue(Math::Color(0.13f, 0.14f, 0.16f, 1.0f)));
|
||||
definition.SetToken("color.surface.elevated", UIStyleValue(Math::Color(0.18f, 0.19f, 0.22f, 1.0f)));
|
||||
definition.SetToken("color.text.primary", UIStyleValue(Math::Color(0.92f, 0.93f, 0.95f, 1.0f)));
|
||||
definition.SetToken("color.accent", UIStyleValue(Math::Color(0.98f, 0.53f, 0.17f, 1.0f)));
|
||||
definition.SetToken("space.compact", UIStyleValue(6.0f));
|
||||
definition.SetToken("space.regular", UIStyleValue(10.0f));
|
||||
definition.SetToken("padding.control", UIStyleValue(UIThickness::Uniform(10.0f)));
|
||||
definition.SetToken("radius.control", UIStyleValue(UICornerRadius::Uniform(6.0f)));
|
||||
definition.SetToken("font.body", UIStyleValue(14.0f));
|
||||
definition.SetToken("line.default", UIStyleValue(1.0f));
|
||||
return BuildTheme(definition);
|
||||
}
|
||||
|
||||
} // namespace Style
|
||||
} // namespace UI
|
||||
} // namespace XCEngine
|
||||
Reference in New Issue
Block a user