From b75afd6d91b2661eccbc536fac7a0063b8dbaeca Mon Sep 17 00:00:00 2001 From: Mike James Date: Mon, 13 Jul 2026 11:12:53 +0200 Subject: [PATCH] Fluent2: derive WinUI tokens from legacy palette colors ColorPaletteResources now answers for the nearest Fluent 2 token colors when legacy palette colors are set (RegionColor -> SolidBackgroundFillColor*, BaseHigh/MediumHigh/Medium -> TextFillColor* with per-variant token alphas, BaseLow -> ControlFillColor*, ErrorText -> SystemFillColorCritical, ChromeMedium -> SolidBackgroundFillColorQuarternary), so v1-era palette customization keeps driving the new visuals. Heuristic by design; exact control is overriding the token keys directly. Co-Authored-By: Claude Fable 5 --- .../ColorPaletteResources.cs | 49 +++++++++++++++++++ .../PaletteTests.cs | 35 +++++++++++++ 2 files changed, 84 insertions(+) diff --git a/src/Avalonia.Themes.Fluent2/ColorPaletteResources.cs b/src/Avalonia.Themes.Fluent2/ColorPaletteResources.cs index 1c87c9ed11..5d10b3a61a 100644 --- a/src/Avalonia.Themes.Fluent2/ColorPaletteResources.cs +++ b/src/Avalonia.Themes.Fluent2/ColorPaletteResources.cs @@ -18,6 +18,31 @@ public partial class ColorPaletteResources : ResourceProvider { private readonly Dictionary _colors = new(StringComparer.InvariantCulture); + private readonly record struct TokenDerivation(string SourceKey, byte? LightAlpha, byte? DarkAlpha); + + // Legacy palette colors also drive the nearest Fluent 2 (WinUI 3) tokens so that + // v1-era palette customization keeps affecting the new visuals. The token's own + // per-variant alpha is applied over the user's RGB; a null alpha keeps the user's. + // This is a documented heuristic — for exact control override the token keys directly. + private static readonly Dictionary s_tokenDerivations = new(StringComparer.InvariantCulture) + { + ["TextFillColorPrimary"] = new[] { new TokenDerivation("SystemBaseHighColor", 0xE4, 0xFF) }, + ["TextFillColorSecondary"] = new[] { new TokenDerivation("SystemBaseMediumHighColor", 0x9E, 0xC5) }, + ["TextFillColorTertiary"] = new[] { new TokenDerivation("SystemBaseMediumColor", 0x72, 0x87) }, + ["ControlFillColorDefault"] = new[] { new TokenDerivation("SystemBaseLowColor", 0xB3, 0x0F) }, + ["ControlFillColorSecondary"] = new[] { new TokenDerivation("SystemBaseLowColor", 0x80, 0x15) }, + ["ControlFillColorTertiary"] = new[] { new TokenDerivation("SystemBaseLowColor", 0x4D, 0x08) }, + ["SolidBackgroundFillColorBase"] = new[] { new TokenDerivation("SystemRegionColor", null, null) }, + ["SolidBackgroundFillColorSecondary"] = new[] { new TokenDerivation("SystemRegionColor", null, null) }, + ["SolidBackgroundFillColorTertiary"] = new[] { new TokenDerivation("SystemRegionColor", null, null) }, + ["SolidBackgroundFillColorQuarternary"] = new[] + { + new TokenDerivation("SystemChromeMediumColor", null, null), + new TokenDerivation("SystemRegionColor", null, null), + }, + ["SystemFillColorCritical"] = new[] { new TokenDerivation("SystemErrorTextColor", null, null) }, + }; + public override bool HasResources => _hasAccentColor || _colors.Count > 0; public override bool TryGetResource(object key, ThemeVariant? theme, out object? value) @@ -71,6 +96,30 @@ public partial class ColorPaletteResources : ResourceProvider value = color; return true; } + + if (TryGetDerivedTokenColor(strKey, theme, out value)) + { + return true; + } + } + + value = null; + return false; + } + + private bool TryGetDerivedTokenColor(string key, ThemeVariant? theme, out object? value) + { + if (s_tokenDerivations.TryGetValue(key, out var derivations)) + { + foreach (var derivation in derivations) + { + if (_colors.TryGetValue(derivation.SourceKey, out var source)) + { + var alpha = theme == ThemeVariant.Dark ? derivation.DarkAlpha : derivation.LightAlpha; + value = alpha is { } a ? new Color(a, source.R, source.G, source.B) : source; + return true; + } + } } value = null; diff --git a/tests/Avalonia.Themes.Fluent2.UnitTests/PaletteTests.cs b/tests/Avalonia.Themes.Fluent2.UnitTests/PaletteTests.cs index 246ef19af3..1acfdc0a29 100644 --- a/tests/Avalonia.Themes.Fluent2.UnitTests/PaletteTests.cs +++ b/tests/Avalonia.Themes.Fluent2.UnitTests/PaletteTests.cs @@ -65,6 +65,41 @@ public class PaletteTests } } + [AvaloniaFact] + public void Legacy_palette_colors_derive_fluent2_tokens() + { + var theme = new Fluent2Theme(); + theme.Palettes[ThemeVariant.Light] = new ColorPaletteResources + { + RegionColor = Color.Parse("#FFF0E0D0"), + BaseHigh = Color.Parse("#FF102030"), + ErrorText = Colors.Orange, + }; + + var app = Application.Current!; + app.Styles.Add(theme); + try + { + Assert.True(app.TryGetResource("SolidBackgroundFillColorBase", ThemeVariant.Light, out var region)); + Assert.Equal(Color.Parse("#FFF0E0D0"), Assert.IsType(region)); + + // BaseHigh drives TextFillColorPrimary with the token's light-variant alpha (0xE4). + Assert.True(app.TryGetResource("TextFillColorPrimary", ThemeVariant.Light, out var textPrimary)); + Assert.Equal(Color.FromArgb(0xE4, 0x10, 0x20, 0x30), Assert.IsType(textPrimary)); + + Assert.True(app.TryGetResource("SystemFillColorCritical", ThemeVariant.Light, out var critical)); + Assert.Equal(Colors.Orange, Assert.IsType(critical)); + + // Legacy keys themselves still answer, as in v1. + Assert.True(app.TryGetResource("SystemBaseHighColor", ThemeVariant.Light, out var baseHigh)); + Assert.Equal(Color.Parse("#FF102030"), Assert.IsType(baseHigh)); + } + finally + { + app.Styles.Remove(theme); + } + } + [AvaloniaFact] public void Compact_density_switches_metric_resources() {