/ src / modules / cmdpal / Microsoft.CmdPal.UI.ViewModels / AppearanceSettingsViewModel.cs
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  }