Add XCUI style theme token system
This commit is contained in:
@@ -32,6 +32,7 @@ add_subdirectory(Math)
|
||||
# Core/UI Tests
|
||||
# ============================================================
|
||||
add_subdirectory(UI)
|
||||
add_subdirectory(UIStyle)
|
||||
|
||||
# Exclude all static runtime libraries to avoid conflicts
|
||||
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