Files
XCEngine/tests/Core/UI/test_style_system.cpp

167 lines
7.7 KiB
C++
Raw Normal View History

#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(UI_StyleSystem, 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(UI_StyleSystem, 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(UI_StyleSystem, 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(UI_StyleSystem, 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(UI_StyleSystem, 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