Add XCUI style theme token system
This commit is contained in:
46
docs/plan/xcui-subplans/Subplan-03_XCUI-Style-Theme-Token.md
Normal file
46
docs/plan/xcui-subplans/Subplan-03_XCUI-Style-Theme-Token.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Subplan 03:XCUI Style / Theme / Token
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 建立 XCUI 的样式模型、token 体系与主题覆盖规则。
|
||||||
|
- 让控件视觉不再散落在代码硬编码常量里。
|
||||||
|
|
||||||
|
负责人边界:
|
||||||
|
|
||||||
|
- 负责 `Style` / `Theme` / `Token` 数据模型和解析规则。
|
||||||
|
- 不负责 `.xcui` 导入器。
|
||||||
|
- 不负责最终 renderer 具体绘制。
|
||||||
|
|
||||||
|
建议目录:
|
||||||
|
|
||||||
|
- `engine/include/XCEngine/UI/Style/`
|
||||||
|
- `engine/src/UI/Style/`
|
||||||
|
- `tests` 中 style/theme 测试
|
||||||
|
|
||||||
|
前置依赖:
|
||||||
|
|
||||||
|
- 依赖 `Subplan 01` 的节点属性注入点。
|
||||||
|
|
||||||
|
现在就可以先做的内容:
|
||||||
|
|
||||||
|
- 设计 style 层级:默认样式、类型样式、命名样式、局部覆盖
|
||||||
|
- 设计 token:颜色、圆角、边距、字号、线宽
|
||||||
|
- 设计主题切换与 token 查询接口
|
||||||
|
- 写冲突优先级测试
|
||||||
|
|
||||||
|
明确不做:
|
||||||
|
|
||||||
|
- 不做具体面板视觉重构
|
||||||
|
- 不做字体资源导入
|
||||||
|
|
||||||
|
交付物:
|
||||||
|
|
||||||
|
- 统一样式查询入口
|
||||||
|
- `.xctheme` 对应的数据结构
|
||||||
|
- 样式优先级与 token 解析测试
|
||||||
|
|
||||||
|
验收标准:
|
||||||
|
|
||||||
|
- 样式优先级可预测
|
||||||
|
- 主题替换不需要改控件逻辑
|
||||||
|
- 其他 subplan 能通过统一 API 获取视觉参数
|
||||||
@@ -413,7 +413,14 @@ add_library(XCEngine STATIC
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Core/UIElementTree.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Core/UIElementTree.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Core/UIContext.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Core/UIContext.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Core/UIBuildContext.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Core/UIBuildContext.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Core/UIElementTree.cpp
|
/src/UI/Core/UIElementTree.cpp
|
||||||
|
/include/XCEngine/UI/Style/StyleTypes.h
|
||||||
|
/include/XCEngine/UI/Style/Theme.h
|
||||||
|
/include/XCEngine/UI/Style/StyleSet.h
|
||||||
|
/include/XCEngine/UI/Style/StyleResolver.h
|
||||||
|
/src/UI/Style/StyleTypes.cpp
|
||||||
|
/src/UI/Style/Theme.cpp
|
||||||
|
/src/UI/Style/StyleResolver.cpp
|
||||||
|
|
||||||
# Input
|
# Input
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Input/InputTypes.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Input/InputTypes.h
|
||||||
|
|||||||
61
engine/include/XCEngine/UI/Style/StyleResolver.h
Normal file
61
engine/include/XCEngine/UI/Style/StyleResolver.h
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "StyleSet.h"
|
||||||
|
#include "Theme.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace UI {
|
||||||
|
namespace Style {
|
||||||
|
|
||||||
|
enum class UIStyleLayer : std::uint8_t {
|
||||||
|
None = 0,
|
||||||
|
Default,
|
||||||
|
Type,
|
||||||
|
Named,
|
||||||
|
Local
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIStylePropertyResolution {
|
||||||
|
bool resolved = false;
|
||||||
|
UIStyleLayer layer = UIStyleLayer::None;
|
||||||
|
UIStyleValue value = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
class UIResolvedStyle {
|
||||||
|
public:
|
||||||
|
void SetProperty(UIStylePropertyId propertyId, const UIStylePropertyResolution& resolution) {
|
||||||
|
m_properties[propertyId] = resolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UIStylePropertyResolution* FindProperty(UIStylePropertyId propertyId) const {
|
||||||
|
const auto it = m_properties.find(propertyId);
|
||||||
|
return it != m_properties.end() ? &it->second : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::map<UIStylePropertyId, UIStylePropertyResolution>& GetProperties() const {
|
||||||
|
return m_properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::map<UIStylePropertyId, UIStylePropertyResolution> m_properties = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIStyleResolveContext {
|
||||||
|
const UITheme* theme = nullptr;
|
||||||
|
const UIStyleSheet* styleSheet = nullptr;
|
||||||
|
UIStyleSelector selector = {};
|
||||||
|
const UIStyleSet* localStyle = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
UIStylePropertyResolution ResolveStyleProperty(
|
||||||
|
UIStylePropertyId propertyId,
|
||||||
|
const UIStyleResolveContext& context);
|
||||||
|
|
||||||
|
UIResolvedStyle ResolveStyle(const UIStyleResolveContext& context);
|
||||||
|
const char* ToString(UIStyleLayer layer);
|
||||||
|
|
||||||
|
} // namespace Style
|
||||||
|
} // namespace UI
|
||||||
|
} // namespace XCEngine
|
||||||
80
engine/include/XCEngine/UI/Style/StyleSet.h
Normal file
80
engine/include/XCEngine/UI/Style/StyleSet.h
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "StyleTypes.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace UI {
|
||||||
|
namespace Style {
|
||||||
|
|
||||||
|
class UIStyleSet {
|
||||||
|
public:
|
||||||
|
void SetProperty(UIStylePropertyId propertyId, const UIStyleValue& value) {
|
||||||
|
m_properties[propertyId] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RemoveProperty(UIStylePropertyId propertyId) {
|
||||||
|
return m_properties.erase(propertyId) > 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasProperty(UIStylePropertyId propertyId) const {
|
||||||
|
return m_properties.find(propertyId) != m_properties.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const UIStyleValue* FindProperty(UIStylePropertyId propertyId) const {
|
||||||
|
const auto it = m_properties.find(propertyId);
|
||||||
|
return it != m_properties.end() ? &it->second : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::map<UIStylePropertyId, UIStyleValue>& GetProperties() const {
|
||||||
|
return m_properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::map<UIStylePropertyId, UIStyleValue> m_properties = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIStyleSelector {
|
||||||
|
std::string typeName;
|
||||||
|
std::string styleName;
|
||||||
|
};
|
||||||
|
|
||||||
|
class UIStyleSheet {
|
||||||
|
public:
|
||||||
|
UIStyleSet& DefaultStyle() {
|
||||||
|
return m_defaultStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UIStyleSet& DefaultStyle() const {
|
||||||
|
return m_defaultStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIStyleSet& GetOrCreateTypeStyle(const std::string& typeName) {
|
||||||
|
return m_typeStyles[typeName];
|
||||||
|
}
|
||||||
|
|
||||||
|
UIStyleSet& GetOrCreateNamedStyle(const std::string& styleName) {
|
||||||
|
return m_namedStyles[styleName];
|
||||||
|
}
|
||||||
|
|
||||||
|
const UIStyleSet* FindTypeStyle(const std::string& typeName) const {
|
||||||
|
const auto it = m_typeStyles.find(typeName);
|
||||||
|
return it != m_typeStyles.end() ? &it->second : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UIStyleSet* FindNamedStyle(const std::string& styleName) const {
|
||||||
|
const auto it = m_namedStyles.find(styleName);
|
||||||
|
return it != m_namedStyles.end() ? &it->second : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
UIStyleSet m_defaultStyle = {};
|
||||||
|
std::map<std::string, UIStyleSet> m_typeStyles = {};
|
||||||
|
std::map<std::string, UIStyleSet> m_namedStyles = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Style
|
||||||
|
} // namespace UI
|
||||||
|
} // namespace XCEngine
|
||||||
104
engine/include/XCEngine/UI/Style/StyleTypes.h
Normal file
104
engine/include/XCEngine/UI/Style/StyleTypes.h
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEngine/Core/Math/Color.h>
|
||||||
|
#include <XCEngine/UI/Types.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace UI {
|
||||||
|
namespace Style {
|
||||||
|
|
||||||
|
struct UIThickness {
|
||||||
|
float left = 0.0f;
|
||||||
|
float top = 0.0f;
|
||||||
|
float right = 0.0f;
|
||||||
|
float bottom = 0.0f;
|
||||||
|
|
||||||
|
static UIThickness Uniform(float value);
|
||||||
|
bool IsUniform() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UICornerRadius {
|
||||||
|
float topLeft = 0.0f;
|
||||||
|
float topRight = 0.0f;
|
||||||
|
float bottomRight = 0.0f;
|
||||||
|
float bottomLeft = 0.0f;
|
||||||
|
|
||||||
|
static UICornerRadius Uniform(float value);
|
||||||
|
bool IsUniform() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UITokenReference {
|
||||||
|
std::string name;
|
||||||
|
|
||||||
|
bool IsValid() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class UIStyleValueType : std::uint8_t {
|
||||||
|
None = 0,
|
||||||
|
Float,
|
||||||
|
Color,
|
||||||
|
Thickness,
|
||||||
|
CornerRadius,
|
||||||
|
Size,
|
||||||
|
Point,
|
||||||
|
TokenReference
|
||||||
|
};
|
||||||
|
|
||||||
|
class UIStyleValue {
|
||||||
|
public:
|
||||||
|
using Storage =
|
||||||
|
std::variant<std::monostate, float, Math::Color, UIThickness, UICornerRadius, UISize, UIPoint, UITokenReference>;
|
||||||
|
|
||||||
|
UIStyleValue() = default;
|
||||||
|
explicit UIStyleValue(float value);
|
||||||
|
explicit UIStyleValue(const Math::Color& value);
|
||||||
|
explicit UIStyleValue(const UIThickness& value);
|
||||||
|
explicit UIStyleValue(const UICornerRadius& value);
|
||||||
|
explicit UIStyleValue(const UISize& value);
|
||||||
|
explicit UIStyleValue(const UIPoint& value);
|
||||||
|
|
||||||
|
static UIStyleValue Token(const std::string& tokenName);
|
||||||
|
|
||||||
|
bool IsSet() const;
|
||||||
|
bool IsTokenReference() const;
|
||||||
|
UIStyleValueType GetType() const;
|
||||||
|
|
||||||
|
const float* TryGetFloat() const;
|
||||||
|
const Math::Color* TryGetColor() const;
|
||||||
|
const UIThickness* TryGetThickness() const;
|
||||||
|
const UICornerRadius* TryGetCornerRadius() const;
|
||||||
|
const UISize* TryGetSize() const;
|
||||||
|
const UIPoint* TryGetPoint() const;
|
||||||
|
const UITokenReference* TryGetTokenReference() const;
|
||||||
|
|
||||||
|
bool operator==(const UIStyleValue& other) const;
|
||||||
|
bool operator!=(const UIStyleValue& other) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit UIStyleValue(const UITokenReference& tokenReference);
|
||||||
|
|
||||||
|
Storage m_storage = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class UIStylePropertyId : std::uint8_t {
|
||||||
|
BackgroundColor = 0,
|
||||||
|
ForegroundColor,
|
||||||
|
BorderColor,
|
||||||
|
BorderWidth,
|
||||||
|
CornerRadius,
|
||||||
|
Padding,
|
||||||
|
Gap,
|
||||||
|
FontSize,
|
||||||
|
LineWidth
|
||||||
|
};
|
||||||
|
|
||||||
|
UIStyleValueType GetExpectedValueType(UIStylePropertyId propertyId);
|
||||||
|
const char* ToString(UIStylePropertyId propertyId);
|
||||||
|
|
||||||
|
} // namespace Style
|
||||||
|
} // namespace UI
|
||||||
|
} // namespace XCEngine
|
||||||
75
engine/include/XCEngine/UI/Style/Theme.h
Normal file
75
engine/include/XCEngine/UI/Style/Theme.h
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "StyleTypes.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace UI {
|
||||||
|
namespace Style {
|
||||||
|
|
||||||
|
enum class UITokenResolveStatus : std::uint8_t {
|
||||||
|
Unresolved = 0,
|
||||||
|
Resolved,
|
||||||
|
MissingToken,
|
||||||
|
CycleDetected,
|
||||||
|
TypeMismatch
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UITokenResolveResult {
|
||||||
|
UITokenResolveStatus status = UITokenResolveStatus::Unresolved;
|
||||||
|
UIStyleValue value = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIThemeDefinition {
|
||||||
|
std::string name;
|
||||||
|
std::map<std::string, UIStyleValue> tokens = {};
|
||||||
|
|
||||||
|
void SetToken(const std::string& tokenName, const UIStyleValue& value);
|
||||||
|
const UIStyleValue* FindToken(const std::string& tokenName) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class UIBuiltinThemeKind : std::uint8_t {
|
||||||
|
NeutralDark = 0,
|
||||||
|
NeutralLight
|
||||||
|
};
|
||||||
|
|
||||||
|
class UITheme {
|
||||||
|
public:
|
||||||
|
UITheme() = default;
|
||||||
|
explicit UITheme(const UIThemeDefinition& definition);
|
||||||
|
|
||||||
|
void SetName(const std::string& name);
|
||||||
|
const std::string& GetName() const;
|
||||||
|
|
||||||
|
void SetParent(const UITheme* parent);
|
||||||
|
const UITheme* GetParent() const;
|
||||||
|
|
||||||
|
void SetToken(const std::string& tokenName, const UIStyleValue& value);
|
||||||
|
bool RemoveToken(const std::string& tokenName);
|
||||||
|
const UIStyleValue* FindToken(const std::string& tokenName) const;
|
||||||
|
const std::map<std::string, UIStyleValue>& GetTokens() const;
|
||||||
|
|
||||||
|
UITokenResolveResult ResolveToken(
|
||||||
|
const std::string& tokenName,
|
||||||
|
UIStyleValueType expectedType = UIStyleValueType::None) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UITokenResolveResult ResolveTokenRecursive(
|
||||||
|
const std::string& tokenName,
|
||||||
|
UIStyleValueType expectedType,
|
||||||
|
std::vector<std::string>& visiting) const;
|
||||||
|
|
||||||
|
std::string m_name;
|
||||||
|
const UITheme* m_parent = nullptr;
|
||||||
|
std::map<std::string, UIStyleValue> m_tokens = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
UITheme BuildTheme(const UIThemeDefinition& definition, const UITheme* parent = nullptr);
|
||||||
|
UITheme BuildBuiltinTheme(UIBuiltinThemeKind themeKind);
|
||||||
|
|
||||||
|
} // namespace Style
|
||||||
|
} // namespace UI
|
||||||
|
} // namespace XCEngine
|
||||||
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
|
||||||
@@ -32,6 +32,7 @@ add_subdirectory(Math)
|
|||||||
# Core/UI Tests
|
# Core/UI Tests
|
||||||
# ============================================================
|
# ============================================================
|
||||||
add_subdirectory(UI)
|
add_subdirectory(UI)
|
||||||
|
add_subdirectory(UIStyle)
|
||||||
|
|
||||||
# Exclude all static runtime libraries to avoid conflicts
|
# Exclude all static runtime libraries to avoid conflicts
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
|
|||||||
30
tests/core/UIStyle/CMakeLists.txt
Normal file
30
tests/core/UIStyle/CMakeLists.txt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# ============================================================
|
||||||
|
# UI Style Tests
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set(UI_STYLE_TEST_SOURCES
|
||||||
|
test_style_system.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(core_ui_style_tests ${UI_STYLE_TEST_SOURCES})
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
set_target_properties(core_ui_style_tests PROPERTIES
|
||||||
|
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(core_ui_style_tests
|
||||||
|
PRIVATE
|
||||||
|
XCEngine
|
||||||
|
GTest::gtest
|
||||||
|
GTest::gtest_main
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(core_ui_style_tests PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/Fixtures
|
||||||
|
)
|
||||||
|
|
||||||
|
include(GoogleTest)
|
||||||
|
gtest_discover_tests(core_ui_style_tests)
|
||||||
166
tests/core/UIStyle/test_style_system.cpp
Normal file
166
tests/core/UIStyle/test_style_system.cpp
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Style/StyleResolver.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::Math::Color;
|
||||||
|
using XCEngine::UI::Style::BuildBuiltinTheme;
|
||||||
|
using XCEngine::UI::Style::BuildTheme;
|
||||||
|
using XCEngine::UI::Style::UICornerRadius;
|
||||||
|
using XCEngine::UI::Style::UIBuiltinThemeKind;
|
||||||
|
using XCEngine::UI::Style::UIResolvedStyle;
|
||||||
|
using XCEngine::UI::Style::UIStyleLayer;
|
||||||
|
using XCEngine::UI::Style::UIStylePropertyId;
|
||||||
|
using XCEngine::UI::Style::UIStyleResolveContext;
|
||||||
|
using XCEngine::UI::Style::UIStyleSet;
|
||||||
|
using XCEngine::UI::Style::UIStyleSheet;
|
||||||
|
using XCEngine::UI::Style::UIStyleValue;
|
||||||
|
using XCEngine::UI::Style::UIStyleValueType;
|
||||||
|
using XCEngine::UI::Style::UITheme;
|
||||||
|
using XCEngine::UI::Style::UIThemeDefinition;
|
||||||
|
using XCEngine::UI::Style::UITokenResolveStatus;
|
||||||
|
|
||||||
|
void ExpectColorEq(const Color& actual, const Color& expected) {
|
||||||
|
EXPECT_FLOAT_EQ(actual.r, expected.r);
|
||||||
|
EXPECT_FLOAT_EQ(actual.g, expected.g);
|
||||||
|
EXPECT_FLOAT_EQ(actual.b, expected.b);
|
||||||
|
EXPECT_FLOAT_EQ(actual.a, expected.a);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIStyleSystem, ThemeDefinitionBuildsThemeAndResolvesAliases) {
|
||||||
|
UIThemeDefinition definition = {};
|
||||||
|
definition.name = "CustomTheme";
|
||||||
|
definition.SetToken("space.base", UIStyleValue(8.0f));
|
||||||
|
definition.SetToken("gap.control", UIStyleValue::Token("space.base"));
|
||||||
|
definition.SetToken("radius.card", UIStyleValue(UICornerRadius::Uniform(12.0f)));
|
||||||
|
|
||||||
|
const UITheme theme = BuildTheme(definition);
|
||||||
|
|
||||||
|
EXPECT_EQ(theme.GetName(), "CustomTheme");
|
||||||
|
|
||||||
|
const auto gapToken = theme.ResolveToken("gap.control", UIStyleValueType::Float);
|
||||||
|
ASSERT_EQ(gapToken.status, UITokenResolveStatus::Resolved);
|
||||||
|
ASSERT_NE(gapToken.value.TryGetFloat(), nullptr);
|
||||||
|
EXPECT_FLOAT_EQ(*gapToken.value.TryGetFloat(), 8.0f);
|
||||||
|
|
||||||
|
const auto radiusToken = theme.ResolveToken("radius.card", UIStyleValueType::CornerRadius);
|
||||||
|
ASSERT_EQ(radiusToken.status, UITokenResolveStatus::Resolved);
|
||||||
|
ASSERT_NE(radiusToken.value.TryGetCornerRadius(), nullptr);
|
||||||
|
EXPECT_TRUE(radiusToken.value.TryGetCornerRadius()->IsUniform());
|
||||||
|
EXPECT_FLOAT_EQ(radiusToken.value.TryGetCornerRadius()->topLeft, 12.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIStyleSystem, ThemeResolvesParentTokensAndBuiltinVariantsDiffer) {
|
||||||
|
UIThemeDefinition baseDefinition = {};
|
||||||
|
baseDefinition.name = "Base";
|
||||||
|
baseDefinition.SetToken("color.surface", UIStyleValue(Color(0.20f, 0.21f, 0.22f, 1.0f)));
|
||||||
|
const UITheme baseTheme = BuildTheme(baseDefinition);
|
||||||
|
|
||||||
|
UIThemeDefinition childDefinition = {};
|
||||||
|
childDefinition.name = "Child";
|
||||||
|
childDefinition.SetToken("font.body", UIStyleValue(15.0f));
|
||||||
|
const UITheme childTheme = BuildTheme(childDefinition, &baseTheme);
|
||||||
|
|
||||||
|
const auto parentToken = childTheme.ResolveToken("color.surface", UIStyleValueType::Color);
|
||||||
|
ASSERT_EQ(parentToken.status, UITokenResolveStatus::Resolved);
|
||||||
|
ASSERT_NE(parentToken.value.TryGetColor(), nullptr);
|
||||||
|
ExpectColorEq(*parentToken.value.TryGetColor(), Color(0.20f, 0.21f, 0.22f, 1.0f));
|
||||||
|
|
||||||
|
const UITheme darkTheme = BuildBuiltinTheme(UIBuiltinThemeKind::NeutralDark);
|
||||||
|
const UITheme lightTheme = BuildBuiltinTheme(UIBuiltinThemeKind::NeutralLight);
|
||||||
|
const auto darkSurface = darkTheme.ResolveToken("color.surface", UIStyleValueType::Color);
|
||||||
|
const auto lightSurface = lightTheme.ResolveToken("color.surface", UIStyleValueType::Color);
|
||||||
|
ASSERT_EQ(darkSurface.status, UITokenResolveStatus::Resolved);
|
||||||
|
ASSERT_EQ(lightSurface.status, UITokenResolveStatus::Resolved);
|
||||||
|
ASSERT_NE(darkSurface.value.TryGetColor(), nullptr);
|
||||||
|
ASSERT_NE(lightSurface.value.TryGetColor(), nullptr);
|
||||||
|
EXPECT_NE(darkSurface.value.TryGetColor()->r, lightSurface.value.TryGetColor()->r);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIStyleSystem, ThemeReportsMissingCyclesAndTypeMismatches) {
|
||||||
|
UIThemeDefinition definition = {};
|
||||||
|
definition.SetToken("cycle.a", UIStyleValue::Token("cycle.b"));
|
||||||
|
definition.SetToken("cycle.b", UIStyleValue::Token("cycle.a"));
|
||||||
|
definition.SetToken("color.surface", UIStyleValue(Color(0.1f, 0.2f, 0.3f, 1.0f)));
|
||||||
|
const UITheme theme = BuildTheme(definition);
|
||||||
|
|
||||||
|
EXPECT_EQ(
|
||||||
|
theme.ResolveToken("missing.token", UIStyleValueType::Float).status,
|
||||||
|
UITokenResolveStatus::MissingToken);
|
||||||
|
EXPECT_EQ(
|
||||||
|
theme.ResolveToken("cycle.a", UIStyleValueType::Float).status,
|
||||||
|
UITokenResolveStatus::CycleDetected);
|
||||||
|
EXPECT_EQ(
|
||||||
|
theme.ResolveToken("color.surface", UIStyleValueType::Float).status,
|
||||||
|
UITokenResolveStatus::TypeMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIStyleSystem, StyleResolutionPrefersLocalThenNamedThenTypeThenDefault) {
|
||||||
|
UIStyleSheet styleSheet = {};
|
||||||
|
styleSheet.DefaultStyle().SetProperty(UIStylePropertyId::FontSize, UIStyleValue(12.0f));
|
||||||
|
styleSheet.GetOrCreateTypeStyle("Button").SetProperty(UIStylePropertyId::FontSize, UIStyleValue(14.0f));
|
||||||
|
styleSheet.GetOrCreateNamedStyle("Primary").SetProperty(UIStylePropertyId::FontSize, UIStyleValue(16.0f));
|
||||||
|
|
||||||
|
UIStyleSet localStyle = {};
|
||||||
|
localStyle.SetProperty(UIStylePropertyId::FontSize, UIStyleValue(18.0f));
|
||||||
|
|
||||||
|
UIStyleResolveContext context = {};
|
||||||
|
context.styleSheet = &styleSheet;
|
||||||
|
context.selector.typeName = "Button";
|
||||||
|
context.selector.styleName = "Primary";
|
||||||
|
context.localStyle = &localStyle;
|
||||||
|
|
||||||
|
auto resolution = XCEngine::UI::Style::ResolveStyleProperty(UIStylePropertyId::FontSize, context);
|
||||||
|
ASSERT_TRUE(resolution.resolved);
|
||||||
|
EXPECT_EQ(resolution.layer, UIStyleLayer::Local);
|
||||||
|
ASSERT_NE(resolution.value.TryGetFloat(), nullptr);
|
||||||
|
EXPECT_FLOAT_EQ(*resolution.value.TryGetFloat(), 18.0f);
|
||||||
|
|
||||||
|
localStyle.RemoveProperty(UIStylePropertyId::FontSize);
|
||||||
|
resolution = XCEngine::UI::Style::ResolveStyleProperty(UIStylePropertyId::FontSize, context);
|
||||||
|
ASSERT_TRUE(resolution.resolved);
|
||||||
|
EXPECT_EQ(resolution.layer, UIStyleLayer::Named);
|
||||||
|
EXPECT_FLOAT_EQ(*resolution.value.TryGetFloat(), 16.0f);
|
||||||
|
|
||||||
|
styleSheet.GetOrCreateNamedStyle("Primary").RemoveProperty(UIStylePropertyId::FontSize);
|
||||||
|
resolution = XCEngine::UI::Style::ResolveStyleProperty(UIStylePropertyId::FontSize, context);
|
||||||
|
ASSERT_TRUE(resolution.resolved);
|
||||||
|
EXPECT_EQ(resolution.layer, UIStyleLayer::Type);
|
||||||
|
EXPECT_FLOAT_EQ(*resolution.value.TryGetFloat(), 14.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIStyleSystem, StyleResolutionFallsBackWhenHigherPriorityTokenCannotResolve) {
|
||||||
|
UIThemeDefinition themeDefinition = {};
|
||||||
|
themeDefinition.SetToken("color.accent", UIStyleValue(Color(0.90f, 0.20f, 0.10f, 1.0f)));
|
||||||
|
const UITheme theme = BuildTheme(themeDefinition);
|
||||||
|
|
||||||
|
UIStyleSheet styleSheet = {};
|
||||||
|
styleSheet.DefaultStyle().SetProperty(UIStylePropertyId::BorderWidth, UIStyleValue(1.0f));
|
||||||
|
styleSheet.GetOrCreateTypeStyle("Button")
|
||||||
|
.SetProperty(UIStylePropertyId::BackgroundColor, UIStyleValue::Token("color.accent"));
|
||||||
|
styleSheet.GetOrCreateNamedStyle("Danger")
|
||||||
|
.SetProperty(UIStylePropertyId::BackgroundColor, UIStyleValue::Token("missing.token"));
|
||||||
|
|
||||||
|
UIStyleResolveContext context = {};
|
||||||
|
context.theme = &theme;
|
||||||
|
context.styleSheet = &styleSheet;
|
||||||
|
context.selector.typeName = "Button";
|
||||||
|
context.selector.styleName = "Danger";
|
||||||
|
|
||||||
|
const auto backgroundResolution =
|
||||||
|
XCEngine::UI::Style::ResolveStyleProperty(UIStylePropertyId::BackgroundColor, context);
|
||||||
|
ASSERT_TRUE(backgroundResolution.resolved);
|
||||||
|
EXPECT_EQ(backgroundResolution.layer, UIStyleLayer::Type);
|
||||||
|
ASSERT_NE(backgroundResolution.value.TryGetColor(), nullptr);
|
||||||
|
ExpectColorEq(*backgroundResolution.value.TryGetColor(), Color(0.90f, 0.20f, 0.10f, 1.0f));
|
||||||
|
|
||||||
|
const UIResolvedStyle resolvedStyle = XCEngine::UI::Style::ResolveStyle(context);
|
||||||
|
const auto* borderWidthResolution = resolvedStyle.FindProperty(UIStylePropertyId::BorderWidth);
|
||||||
|
ASSERT_NE(borderWidthResolution, nullptr);
|
||||||
|
EXPECT_EQ(borderWidthResolution->layer, UIStyleLayer::Default);
|
||||||
|
ASSERT_NE(borderWidthResolution->value.TryGetFloat(), nullptr);
|
||||||
|
EXPECT_FLOAT_EQ(*borderWidthResolution->value.TryGetFloat(), 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
Reference in New Issue
Block a user