167 lines
7.7 KiB
C++
167 lines
7.7 KiB
C++
|
|
#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
|