#include #include 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