AppearanceSettingsViewModel.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 System.Collections.ObjectModel; 6 using CommunityToolkit.Mvvm.ComponentModel; 7 using CommunityToolkit.Mvvm.Input; 8 using CommunityToolkit.WinUI; 9 using Microsoft.CmdPal.UI.ViewModels.Services; 10 using Microsoft.UI; 11 using Microsoft.UI.Dispatching; 12 using Microsoft.UI.Xaml; 13 using Microsoft.UI.Xaml.Media; 14 using Windows.UI; 15 using Windows.UI.ViewManagement; 16 17 namespace Microsoft.CmdPal.UI.ViewModels; 18 19 public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable 20 { 21 private static readonly ObservableCollection<Color> WindowsColorSwatches = [ 22 23 // row 0 24 Color.FromArgb(255, 255, 185, 0), // #ffb900 25 Color.FromArgb(255, 255, 140, 0), // #ff8c00 26 Color.FromArgb(255, 247, 99, 12), // #f7630c 27 Color.FromArgb(255, 202, 80, 16), // #ca5010 28 Color.FromArgb(255, 218, 59, 1), // #da3b01 29 Color.FromArgb(255, 239, 105, 80), // #ef6950 30 31 // row 1 32 Color.FromArgb(255, 209, 52, 56), // #d13438 33 Color.FromArgb(255, 255, 67, 67), // #ff4343 34 Color.FromArgb(255, 231, 72, 86), // #e74856 35 Color.FromArgb(255, 232, 17, 35), // #e81123 36 Color.FromArgb(255, 234, 0, 94), // #ea005e 37 Color.FromArgb(255, 195, 0, 82), // #c30052 38 39 // row 2 40 Color.FromArgb(255, 227, 0, 140), // #e3008c 41 Color.FromArgb(255, 191, 0, 119), // #bf0077 42 Color.FromArgb(255, 194, 57, 179), // #c239b3 43 Color.FromArgb(255, 154, 0, 137), // #9a0089 44 Color.FromArgb(255, 0, 120, 212), // #0078d4 45 Color.FromArgb(255, 0, 99, 177), // #0063b1 46 47 // row 3 48 Color.FromArgb(255, 142, 140, 216), // #8e8cd8 49 Color.FromArgb(255, 107, 105, 214), // #6b69d6 50 Color.FromArgb(255, 135, 100, 184), // #8764b8 51 Color.FromArgb(255, 116, 77, 169), // #744da9 52 Color.FromArgb(255, 177, 70, 194), // #b146c2 53 Color.FromArgb(255, 136, 23, 152), // #881798 54 55 // row 4 56 Color.FromArgb(255, 0, 153, 188), // #0099bc 57 Color.FromArgb(255, 45, 125, 154), // #2d7d9a 58 Color.FromArgb(255, 0, 183, 195), // #00b7c3 59 Color.FromArgb(255, 3, 131, 135), // #038387 60 Color.FromArgb(255, 0, 178, 148), // #00b294 61 Color.FromArgb(255, 1, 133, 116), // #018574 62 63 // row 5 64 Color.FromArgb(255, 0, 204, 106), // #00cc6a 65 Color.FromArgb(255, 16, 137, 62), // #10893e 66 Color.FromArgb(255, 122, 117, 116), // #7a7574 67 Color.FromArgb(255, 93, 90, 88), // #5d5a58 68 Color.FromArgb(255, 104, 118, 138), // #68768a 69 Color.FromArgb(255, 81, 92, 107), // #515c6b 70 71 // row 6 72 Color.FromArgb(255, 86, 124, 115), // #567c73 73 Color.FromArgb(255, 72, 104, 96), // #486860 74 Color.FromArgb(255, 73, 130, 5), // #498205 75 Color.FromArgb(255, 16, 124, 16), // #107c10 76 Color.FromArgb(255, 118, 118, 118), // #767676 77 Color.FromArgb(255, 76, 74, 72), // #4c4a48 78 79 // row 7 80 Color.FromArgb(255, 105, 121, 126), // #69797e 81 Color.FromArgb(255, 74, 84, 89), // #4a5459 82 Color.FromArgb(255, 100, 124, 100), // #647c64 83 Color.FromArgb(255, 82, 94, 84), // #525e54 84 Color.FromArgb(255, 132, 117, 69), // #847545 85 Color.FromArgb(255, 126, 115, 95), // #7e735f 86 ]; 87 88 private readonly SettingsModel _settings; 89 private readonly UISettings _uiSettings; 90 private readonly IThemeService _themeService; 91 private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer(); 92 private readonly DispatcherQueue _uiDispatcher = DispatcherQueue.GetForCurrentThread(); 93 94 private ElementTheme? _elementThemeOverride; 95 private Color _currentSystemAccentColor; 96 97 public ObservableCollection<Color> Swatches => WindowsColorSwatches; 98 99 public int ThemeIndex 100 { 101 get => (int)_settings.Theme; 102 set => Theme = (UserTheme)value; 103 } 104 105 public UserTheme Theme 106 { 107 get => _settings.Theme; 108 set 109 { 110 if (_settings.Theme != value) 111 { 112 _settings.Theme = value; 113 OnPropertyChanged(); 114 OnPropertyChanged(nameof(ThemeIndex)); 115 Save(); 116 } 117 } 118 } 119 120 public ColorizationMode ColorizationMode 121 { 122 get => _settings.ColorizationMode; 123 set 124 { 125 if (_settings.ColorizationMode != value) 126 { 127 _settings.ColorizationMode = value; 128 OnPropertyChanged(); 129 OnPropertyChanged(nameof(ColorizationModeIndex)); 130 OnPropertyChanged(nameof(IsCustomTintVisible)); 131 OnPropertyChanged(nameof(IsCustomTintIntensityVisible)); 132 OnPropertyChanged(nameof(IsBackgroundControlsVisible)); 133 OnPropertyChanged(nameof(IsNoBackgroundVisible)); 134 OnPropertyChanged(nameof(IsAccentColorControlsVisible)); 135 136 if (value == ColorizationMode.WindowsAccentColor) 137 { 138 ThemeColor = _currentSystemAccentColor; 139 } 140 141 IsColorizationDetailsExpanded = value != ColorizationMode.None; 142 143 Save(); 144 } 145 } 146 } 147 148 public int ColorizationModeIndex 149 { 150 get => (int)_settings.ColorizationMode; 151 set => ColorizationMode = (ColorizationMode)value; 152 } 153 154 public Color ThemeColor 155 { 156 get => _settings.CustomThemeColor; 157 set 158 { 159 if (_settings.CustomThemeColor != value) 160 { 161 _settings.CustomThemeColor = value; 162 163 OnPropertyChanged(); 164 165 if (ColorIntensity == 0) 166 { 167 ColorIntensity = 100; 168 } 169 170 Save(); 171 } 172 } 173 } 174 175 public int ColorIntensity 176 { 177 get => _settings.CustomThemeColorIntensity; 178 set 179 { 180 _settings.CustomThemeColorIntensity = value; 181 OnPropertyChanged(); 182 Save(); 183 } 184 } 185 186 public string BackgroundImagePath 187 { 188 get => _settings.BackgroundImagePath ?? string.Empty; 189 set 190 { 191 if (_settings.BackgroundImagePath != value) 192 { 193 _settings.BackgroundImagePath = value; 194 OnPropertyChanged(); 195 196 if (BackgroundImageOpacity == 0) 197 { 198 BackgroundImageOpacity = 100; 199 } 200 201 Save(); 202 } 203 } 204 } 205 206 public int BackgroundImageOpacity 207 { 208 get => _settings.BackgroundImageOpacity; 209 set 210 { 211 if (_settings.BackgroundImageOpacity != value) 212 { 213 _settings.BackgroundImageOpacity = value; 214 OnPropertyChanged(); 215 Save(); 216 } 217 } 218 } 219 220 public int BackgroundImageBrightness 221 { 222 get => _settings.BackgroundImageBrightness; 223 set 224 { 225 if (_settings.BackgroundImageBrightness != value) 226 { 227 _settings.BackgroundImageBrightness = value; 228 OnPropertyChanged(); 229 Save(); 230 } 231 } 232 } 233 234 public int BackgroundImageBlurAmount 235 { 236 get => _settings.BackgroundImageBlurAmount; 237 set 238 { 239 if (_settings.BackgroundImageBlurAmount != value) 240 { 241 _settings.BackgroundImageBlurAmount = value; 242 OnPropertyChanged(); 243 Save(); 244 } 245 } 246 } 247 248 public BackgroundImageFit BackgroundImageFit 249 { 250 get => _settings.BackgroundImageFit; 251 set 252 { 253 if (_settings.BackgroundImageFit != value) 254 { 255 _settings.BackgroundImageFit = value; 256 OnPropertyChanged(); 257 OnPropertyChanged(nameof(BackgroundImageFitIndex)); 258 Save(); 259 } 260 } 261 } 262 263 public int BackgroundImageFitIndex 264 { 265 // Naming between UI facing string and enum is a bit confusing, but the enum fields 266 // are based on XAML Stretch enum values. So I'm choosing to keep the confusion here, close 267 // to the UI. 268 // - BackgroundImageFit.Fill corresponds to "Stretch" 269 // - BackgroundImageFit.UniformToFill corresponds to "Fill" 270 get => BackgroundImageFit switch 271 { 272 BackgroundImageFit.Fill => 1, 273 _ => 0, 274 }; 275 set => BackgroundImageFit = value switch 276 { 277 1 => BackgroundImageFit.Fill, 278 _ => BackgroundImageFit.UniformToFill, 279 }; 280 } 281 282 [ObservableProperty] 283 public partial bool IsColorizationDetailsExpanded { get; set; } 284 285 public bool IsCustomTintVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image; 286 287 public bool IsCustomTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image; 288 289 public bool IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image; 290 291 public bool IsNoBackgroundVisible => _settings.ColorizationMode is ColorizationMode.None; 292 293 public bool IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor; 294 295 public AcrylicBackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f); 296 297 public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme; 298 299 public Color EffectiveThemeColor => ColorizationMode switch 300 { 301 ColorizationMode.WindowsAccentColor => _currentSystemAccentColor, 302 ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor, 303 _ => Colors.Transparent, 304 }; 305 306 // Since the blur amount is absolute, we need to scale it down for the preview (which is smaller than full screen). 307 public int EffectiveBackgroundImageBlurAmount => (int)Math.Round(BackgroundImageBlurAmount / 4f); 308 309 public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0; 310 311 public ImageSource? EffectiveBackgroundImageSource => 312 ColorizationMode is ColorizationMode.Image 313 && !string.IsNullOrWhiteSpace(BackgroundImagePath) 314 && Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri) 315 ? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri) 316 : null; 317 318 public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings) 319 { 320 _themeService = themeService; 321 _themeService.ThemeChanged += ThemeServiceOnThemeChanged; 322 _settings = settings; 323 324 _uiSettings = new UISettings(); 325 _uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged; 326 UpdateAccentColor(_uiSettings); 327 328 Reapply(); 329 330 IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None; 331 } 332 333 private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender)); 334 335 private void UpdateAccentColor(UISettings sender) 336 { 337 _currentSystemAccentColor = sender.GetColorValue(UIColorType.Accent); 338 if (ColorizationMode == ColorizationMode.WindowsAccentColor) 339 { 340 ThemeColor = _currentSystemAccentColor; 341 } 342 } 343 344 private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e) 345 { 346 _saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200)); 347 } 348 349 private void Save() 350 { 351 SettingsModel.SaveSettings(_settings); 352 _saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200)); 353 } 354 355 private void Reapply() 356 { 357 // Theme services recalculates effective color and opacity based on current settings. 358 EffectiveBackdrop = _themeService.Current.BackdropParameters; 359 OnPropertyChanged(nameof(EffectiveBackdrop)); 360 OnPropertyChanged(nameof(EffectiveBackgroundImageBrightness)); 361 OnPropertyChanged(nameof(EffectiveBackgroundImageSource)); 362 OnPropertyChanged(nameof(EffectiveThemeColor)); 363 OnPropertyChanged(nameof(EffectiveBackgroundImageBlurAmount)); 364 365 // LOAD BEARING: 366 // We need to cycle through the EffectiveTheme property to force reload of resources. 367 _elementThemeOverride = ElementTheme.Light; 368 OnPropertyChanged(nameof(EffectiveTheme)); 369 _elementThemeOverride = ElementTheme.Dark; 370 OnPropertyChanged(nameof(EffectiveTheme)); 371 _elementThemeOverride = null; 372 OnPropertyChanged(nameof(EffectiveTheme)); 373 } 374 375 [RelayCommand] 376 private void ResetBackgroundImageProperties() 377 { 378 BackgroundImageBrightness = 0; 379 BackgroundImageBlurAmount = 0; 380 BackgroundImageFit = BackgroundImageFit.UniformToFill; 381 BackgroundImageOpacity = 100; 382 ColorIntensity = 0; 383 } 384 385 public void Dispose() 386 { 387 _uiSettings.ColorValuesChanged -= UiSettingsOnColorValuesChanged; 388 _themeService.ThemeChanged -= ThemeServiceOnThemeChanged; 389 } 390 }