ColorfulThemeProvider.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using CommunityToolkit.WinUI.Helpers; 6 using Microsoft.CmdPal.UI.Helpers; 7 using Microsoft.CmdPal.UI.ViewModels.Services; 8 using Microsoft.UI.Xaml; 9 using Windows.UI; 10 using Windows.UI.ViewManagement; 11 12 namespace Microsoft.CmdPal.UI.Services; 13 14 /// <summary> 15 /// Provides theme appropriate for colorful (accented) appearance. 16 /// </summary> 17 internal sealed class ColorfulThemeProvider : IThemeProvider 18 { 19 // Fluent dark: #202020 20 private static readonly Color DarkBaseColor = Color.FromArgb(255, 32, 32, 32); 21 22 // Fluent light: #F3F3F3 23 private static readonly Color LightBaseColor = Color.FromArgb(255, 243, 243, 243); 24 25 private readonly UISettings _uiSettings; 26 27 public string ThemeKey => "colorful"; 28 29 public string ResourcePath => "ms-appx:///Styles/Theme.Colorful.xaml"; 30 31 public ColorfulThemeProvider(UISettings uiSettings) 32 { 33 ArgumentNullException.ThrowIfNull(uiSettings); 34 _uiSettings = uiSettings; 35 } 36 37 public AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context) 38 { 39 var isLight = context.Theme == ElementTheme.Light || 40 (context.Theme == ElementTheme.Default && 41 _uiSettings.GetColorValue(UIColorType.Background).R > 128); 42 43 var baseColor = isLight ? LightBaseColor : DarkBaseColor; 44 45 // Windows is warping the hue of accent colors and running it through some curves to produce their accent shades. 46 // This will attempt to mimic that behavior. 47 var accentShades = AccentShades.Compute(context.Tint.LerpHsv(WindowsAccentHueWarpTransform.Transform(context.Tint), 0.5f)); 48 var blended = isLight ? accentShades.Light3 : accentShades.Dark2; 49 var colorIntensityUser = (context.ColorIntensity ?? 100) / 100f; 50 51 // For light theme, we want to reduce intensity a bit, and also we need to keep the color fairly light, 52 // to avoid issues with text box caret. 53 var colorIntensity = isLight ? 0.6f * colorIntensityUser : colorIntensityUser; 54 var effectiveBgColor = ColorBlender.Blend(baseColor, blended, colorIntensity); 55 56 return new AcrylicBackdropParameters(effectiveBgColor, effectiveBgColor, 0.8f, 0.8f); 57 } 58 59 private static class ColorBlender 60 { 61 /// <summary> 62 /// Blends a semitransparent tint color over an opaque base color using alpha compositing. 63 /// </summary> 64 /// <param name="baseColor">The opaque base color (background)</param> 65 /// <param name="tintColor">The semitransparent tint color (foreground)</param> 66 /// <param name="intensity">The intensity of the tint (0.0 - 1.0)</param> 67 /// <returns>The resulting blended color</returns> 68 public static Color Blend(Color baseColor, Color tintColor, float intensity) 69 { 70 // Normalize alpha to 0.0 - 1.0 range 71 intensity = Math.Clamp(intensity, 0f, 1f); 72 73 // Alpha compositing formula: result = tint * alpha + base * (1 - alpha) 74 var r = (byte)((tintColor.R * intensity) + (baseColor.R * (1 - intensity))); 75 var g = (byte)((tintColor.G * intensity) + (baseColor.G * (1 - intensity))); 76 var b = (byte)((tintColor.B * intensity) + (baseColor.B * (1 - intensity))); 77 78 // Result is fully opaque since base is opaque 79 return Color.FromArgb(255, r, g, b); 80 } 81 } 82 83 private static class WindowsAccentHueWarpTransform 84 { 85 private static readonly (double HIn, double HOut)[] HueMap = 86 [ 87 (0, 0), 88 (10, 1), 89 (20, 6), 90 (30, 10), 91 (40, 14), 92 (50, 19), 93 (60, 36), 94 (70, 94), 95 (80, 112), 96 (90, 120), 97 (100, 120), 98 (110, 120), 99 (120, 120), 100 (130, 120), 101 (140, 120), 102 (150, 125), 103 (160, 135), 104 (170, 142), 105 (180, 178), 106 (190, 205), 107 (200, 220), 108 (210, 229), 109 (220, 237), 110 (230, 241), 111 (240, 243), 112 (250, 244), 113 (260, 245), 114 (270, 248), 115 (280, 252), 116 (290, 276), 117 (300, 293), 118 (310, 313), 119 (320, 330), 120 (330, 349), 121 (340, 353), 122 (350, 357) 123 ]; 124 125 public static Color Transform(Color input, Options? opt = null) 126 { 127 opt ??= new Options(); 128 var hsv = input.ToHsv(); 129 return ColorHelper.FromHsv( 130 RemapHueLut(hsv.H), 131 Clamp01(Math.Pow(hsv.S, opt.SaturationGamma) * opt.SaturationGain), 132 Clamp01((opt.ValueScaleA * hsv.V) + opt.ValueBiasB), 133 input.A); 134 } 135 136 // Hue LUT remap (piecewise-linear with cyclic wrap) 137 private static double RemapHueLut(double hDeg) 138 { 139 // Normalize to [0,360) 140 hDeg = Mod(hDeg, 360.0); 141 142 // Handle wrap-around case: hDeg is between last entry (350°) and 360° 143 var last = HueMap[^1]; 144 var first = HueMap[0]; 145 if (hDeg >= last.HIn) 146 { 147 // Interpolate between last entry and first entry (wrapped by 360°) 148 var t = (hDeg - last.HIn) / (first.HIn + 360.0 - last.HIn + 1e-12); 149 var ho = Lerp(last.HOut, first.HOut + 360.0, t); 150 return Mod(ho, 360.0); 151 } 152 153 // Find segment [i, i+1] where HueMap[i].HIn <= hDeg < HueMap[i+1].HIn 154 for (var i = 0; i < HueMap.Length - 1; i++) 155 { 156 var a = HueMap[i]; 157 var b = HueMap[i + 1]; 158 159 if (hDeg >= a.HIn && hDeg < b.HIn) 160 { 161 var t = (hDeg - a.HIn) / (b.HIn - a.HIn + 1e-12); 162 return Lerp(a.HOut, b.HOut, t); 163 } 164 } 165 166 // Fallback (shouldn't happen) 167 return hDeg; 168 } 169 170 private static double Lerp(double a, double b, double t) => a + ((b - a) * t); 171 172 private static double Mod(double x, double m) => ((x % m) + m) % m; 173 174 private static double Clamp01(double x) => x < 0 ? 0 : (x > 1 ? 1 : x); 175 176 public sealed class Options 177 { 178 // Saturation boost (1.0 = no change). Typical: 1.3–1.8 179 public double SaturationGain { get; init; } = 1.0; 180 181 // Optional saturation gamma (1.0 = linear). <1.0 raises low S a bit; >1.0 preserves low S. 182 public double SaturationGamma { get; init; } = 1.0; 183 184 // Value (V) remap: V' = a*V + b (tone curve; clamp applied) 185 // Example that lifts blacks & compresses whites slightly: a=0.50, b=0.08 186 public double ValueScaleA { get; init; } = 0.6; 187 188 public double ValueBiasB { get; init; } = 0.01; 189 } 190 } 191 192 private static class AccentShades 193 { 194 public static (Color Light3, Color Light2, Color Light1, Color Dark1, Color Dark2, Color Dark3) Compute(Color accent) 195 { 196 var light1 = accent.Update(brightnessFactor: 0.15, saturationFactor: -0.12); 197 var light2 = accent.Update(brightnessFactor: 0.30, saturationFactor: -0.24); 198 var light3 = accent.Update(brightnessFactor: 0.45, saturationFactor: -0.36); 199 200 var dark1 = accent.UpdateBrightness(brightnessFactor: -0.05f); 201 var dark2 = accent.UpdateBrightness(brightnessFactor: -0.01f); 202 var dark3 = accent.UpdateBrightness(brightnessFactor: -0.015f); 203 204 return (light3, light2, light1, dark1, dark2, dark3); 205 } 206 } 207 }