diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 672616c8e7..4a3305217e 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -141,6 +141,7 @@ BITSPIXEL
bla
BLACKFRAME
BLENDFUNCTION
+blittable
Blockquotes
blt
BLURBEHIND
@@ -250,6 +251,7 @@ colorformat
colorhistory
colorhistorylimit
COLORKEY
+colorref
comctl
comdlg
comexp
@@ -1860,8 +1862,10 @@ Uniquifies
unitconverter
unittests
UNLEN
+Uninitializes
UNORM
unremapped
+Unsubscribes
unvirtualized
unwide
unzoom
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 3d64052a21..eb04903b7e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -40,6 +40,7 @@
+
diff --git a/src/common/ManagedCsWin32/CLSID.cs b/src/common/ManagedCsWin32/CLSID.cs
index 6087ba575b..00315fe737 100644
--- a/src/common/ManagedCsWin32/CLSID.cs
+++ b/src/common/ManagedCsWin32/CLSID.cs
@@ -16,4 +16,5 @@ public static partial class CLSID
public static readonly Guid CollatorDataSource = new Guid("9E175B8B-F52A-11D8-B9A5-505054503030");
public static readonly Guid ApplicationActivationManager = new Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C");
public static readonly Guid VirtualDesktopManager = new("aa509086-5ca9-4c25-8f95-589d3c07b48a");
+ public static readonly Guid DesktopWallpaper = new("C2CF3110-460E-4FC1-B9D0-8A1C0C9CC4BD");
}
diff --git a/src/common/ManagedCsWin32/Ole32.cs b/src/common/ManagedCsWin32/Ole32.cs
index 20181f3626..cf56c80373 100644
--- a/src/common/ManagedCsWin32/Ole32.cs
+++ b/src/common/ManagedCsWin32/Ole32.cs
@@ -16,6 +16,12 @@ public static partial class Ole32
CLSCTX dwClsContext,
ref Guid riid,
out IntPtr rReturnedComObject);
+
+ [LibraryImport("ole32.dll")]
+ internal static partial int CoInitializeEx(nint pvReserved, uint dwCoInit);
+
+ [LibraryImport("ole32.dll")]
+ internal static partial void CoUninitialize();
}
[Flags]
diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs
index 2abbd83d3e..16ca5b1fca 100644
--- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs
+++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs
@@ -14,6 +14,7 @@ using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Core.ViewModels;
public partial class ShellViewModel : ObservableObject,
+ IDisposable,
IRecipient,
IRecipient
{
@@ -460,4 +461,12 @@ public partial class ShellViewModel : ObservableObject,
{
_navigationCts?.Cancel();
}
+
+ public void Dispose()
+ {
+ _handleInvokeTask?.Dispose();
+ _navigationCts?.Dispose();
+
+ GC.SuppressFinalize(this);
+ }
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppearanceSettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppearanceSettingsViewModel.cs
new file mode 100644
index 0000000000..71e150a7d2
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppearanceSettingsViewModel.cs
@@ -0,0 +1,390 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CommunityToolkit.WinUI;
+using Microsoft.CmdPal.UI.ViewModels.Services;
+using Microsoft.UI;
+using Microsoft.UI.Dispatching;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Media;
+using Windows.UI;
+using Windows.UI.ViewManagement;
+
+namespace Microsoft.CmdPal.UI.ViewModels;
+
+public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable
+{
+ private static readonly ObservableCollection WindowsColorSwatches = [
+
+ // row 0
+ Color.FromArgb(255, 255, 185, 0), // #ffb900
+ Color.FromArgb(255, 255, 140, 0), // #ff8c00
+ Color.FromArgb(255, 247, 99, 12), // #f7630c
+ Color.FromArgb(255, 202, 80, 16), // #ca5010
+ Color.FromArgb(255, 218, 59, 1), // #da3b01
+ Color.FromArgb(255, 239, 105, 80), // #ef6950
+
+ // row 1
+ Color.FromArgb(255, 209, 52, 56), // #d13438
+ Color.FromArgb(255, 255, 67, 67), // #ff4343
+ Color.FromArgb(255, 231, 72, 86), // #e74856
+ Color.FromArgb(255, 232, 17, 35), // #e81123
+ Color.FromArgb(255, 234, 0, 94), // #ea005e
+ Color.FromArgb(255, 195, 0, 82), // #c30052
+
+ // row 2
+ Color.FromArgb(255, 227, 0, 140), // #e3008c
+ Color.FromArgb(255, 191, 0, 119), // #bf0077
+ Color.FromArgb(255, 194, 57, 179), // #c239b3
+ Color.FromArgb(255, 154, 0, 137), // #9a0089
+ Color.FromArgb(255, 0, 120, 212), // #0078d4
+ Color.FromArgb(255, 0, 99, 177), // #0063b1
+
+ // row 3
+ Color.FromArgb(255, 142, 140, 216), // #8e8cd8
+ Color.FromArgb(255, 107, 105, 214), // #6b69d6
+ Color.FromArgb(255, 135, 100, 184), // #8764b8
+ Color.FromArgb(255, 116, 77, 169), // #744da9
+ Color.FromArgb(255, 177, 70, 194), // #b146c2
+ Color.FromArgb(255, 136, 23, 152), // #881798
+
+ // row 4
+ Color.FromArgb(255, 0, 153, 188), // #0099bc
+ Color.FromArgb(255, 45, 125, 154), // #2d7d9a
+ Color.FromArgb(255, 0, 183, 195), // #00b7c3
+ Color.FromArgb(255, 3, 131, 135), // #038387
+ Color.FromArgb(255, 0, 178, 148), // #00b294
+ Color.FromArgb(255, 1, 133, 116), // #018574
+
+ // row 5
+ Color.FromArgb(255, 0, 204, 106), // #00cc6a
+ Color.FromArgb(255, 16, 137, 62), // #10893e
+ Color.FromArgb(255, 122, 117, 116), // #7a7574
+ Color.FromArgb(255, 93, 90, 88), // #5d5a58
+ Color.FromArgb(255, 104, 118, 138), // #68768a
+ Color.FromArgb(255, 81, 92, 107), // #515c6b
+
+ // row 6
+ Color.FromArgb(255, 86, 124, 115), // #567c73
+ Color.FromArgb(255, 72, 104, 96), // #486860
+ Color.FromArgb(255, 73, 130, 5), // #498205
+ Color.FromArgb(255, 16, 124, 16), // #107c10
+ Color.FromArgb(255, 118, 118, 118), // #767676
+ Color.FromArgb(255, 76, 74, 72), // #4c4a48
+
+ // row 7
+ Color.FromArgb(255, 105, 121, 126), // #69797e
+ Color.FromArgb(255, 74, 84, 89), // #4a5459
+ Color.FromArgb(255, 100, 124, 100), // #647c64
+ Color.FromArgb(255, 82, 94, 84), // #525e54
+ Color.FromArgb(255, 132, 117, 69), // #847545
+ Color.FromArgb(255, 126, 115, 95), // #7e735f
+ ];
+
+ private readonly SettingsModel _settings;
+ private readonly UISettings _uiSettings;
+ private readonly IThemeService _themeService;
+ private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
+ private readonly DispatcherQueue _uiDispatcher = DispatcherQueue.GetForCurrentThread();
+
+ private ElementTheme? _elementThemeOverride;
+ private Color _currentSystemAccentColor;
+
+ public ObservableCollection Swatches => WindowsColorSwatches;
+
+ public int ThemeIndex
+ {
+ get => (int)_settings.Theme;
+ set => Theme = (UserTheme)value;
+ }
+
+ public UserTheme Theme
+ {
+ get => _settings.Theme;
+ set
+ {
+ if (_settings.Theme != value)
+ {
+ _settings.Theme = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(ThemeIndex));
+ Save();
+ }
+ }
+ }
+
+ public ColorizationMode ColorizationMode
+ {
+ get => _settings.ColorizationMode;
+ set
+ {
+ if (_settings.ColorizationMode != value)
+ {
+ _settings.ColorizationMode = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(ColorizationModeIndex));
+ OnPropertyChanged(nameof(IsCustomTintVisible));
+ OnPropertyChanged(nameof(IsCustomTintIntensityVisible));
+ OnPropertyChanged(nameof(IsBackgroundControlsVisible));
+ OnPropertyChanged(nameof(IsNoBackgroundVisible));
+ OnPropertyChanged(nameof(IsAccentColorControlsVisible));
+
+ if (value == ColorizationMode.WindowsAccentColor)
+ {
+ ThemeColor = _currentSystemAccentColor;
+ }
+
+ IsColorizationDetailsExpanded = value != ColorizationMode.None;
+
+ Save();
+ }
+ }
+ }
+
+ public int ColorizationModeIndex
+ {
+ get => (int)_settings.ColorizationMode;
+ set => ColorizationMode = (ColorizationMode)value;
+ }
+
+ public Color ThemeColor
+ {
+ get => _settings.CustomThemeColor;
+ set
+ {
+ if (_settings.CustomThemeColor != value)
+ {
+ _settings.CustomThemeColor = value;
+
+ OnPropertyChanged();
+
+ if (ColorIntensity == 0)
+ {
+ ColorIntensity = 100;
+ }
+
+ Save();
+ }
+ }
+ }
+
+ public int ColorIntensity
+ {
+ get => _settings.CustomThemeColorIntensity;
+ set
+ {
+ _settings.CustomThemeColorIntensity = value;
+ OnPropertyChanged();
+ Save();
+ }
+ }
+
+ public string BackgroundImagePath
+ {
+ get => _settings.BackgroundImagePath ?? string.Empty;
+ set
+ {
+ if (_settings.BackgroundImagePath != value)
+ {
+ _settings.BackgroundImagePath = value;
+ OnPropertyChanged();
+
+ if (BackgroundImageOpacity == 0)
+ {
+ BackgroundImageOpacity = 100;
+ }
+
+ Save();
+ }
+ }
+ }
+
+ public int BackgroundImageOpacity
+ {
+ get => _settings.BackgroundImageOpacity;
+ set
+ {
+ if (_settings.BackgroundImageOpacity != value)
+ {
+ _settings.BackgroundImageOpacity = value;
+ OnPropertyChanged();
+ Save();
+ }
+ }
+ }
+
+ public int BackgroundImageBrightness
+ {
+ get => _settings.BackgroundImageBrightness;
+ set
+ {
+ if (_settings.BackgroundImageBrightness != value)
+ {
+ _settings.BackgroundImageBrightness = value;
+ OnPropertyChanged();
+ Save();
+ }
+ }
+ }
+
+ public int BackgroundImageBlurAmount
+ {
+ get => _settings.BackgroundImageBlurAmount;
+ set
+ {
+ if (_settings.BackgroundImageBlurAmount != value)
+ {
+ _settings.BackgroundImageBlurAmount = value;
+ OnPropertyChanged();
+ Save();
+ }
+ }
+ }
+
+ public BackgroundImageFit BackgroundImageFit
+ {
+ get => _settings.BackgroundImageFit;
+ set
+ {
+ if (_settings.BackgroundImageFit != value)
+ {
+ _settings.BackgroundImageFit = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(BackgroundImageFitIndex));
+ Save();
+ }
+ }
+ }
+
+ public int BackgroundImageFitIndex
+ {
+ // Naming between UI facing string and enum is a bit confusing, but the enum fields
+ // are based on XAML Stretch enum values. So I'm choosing to keep the confusion here, close
+ // to the UI.
+ // - BackgroundImageFit.Fill corresponds to "Stretch"
+ // - BackgroundImageFit.UniformToFill corresponds to "Fill"
+ get => BackgroundImageFit switch
+ {
+ BackgroundImageFit.Fill => 1,
+ _ => 0,
+ };
+ set => BackgroundImageFit = value switch
+ {
+ 1 => BackgroundImageFit.Fill,
+ _ => BackgroundImageFit.UniformToFill,
+ };
+ }
+
+ [ObservableProperty]
+ public partial bool IsColorizationDetailsExpanded { get; set; }
+
+ public bool IsCustomTintVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
+
+ public bool IsCustomTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
+
+ public bool IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image;
+
+ public bool IsNoBackgroundVisible => _settings.ColorizationMode is ColorizationMode.None;
+
+ public bool IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
+
+ public AcrylicBackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
+
+ public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
+
+ public Color EffectiveThemeColor => ColorizationMode switch
+ {
+ ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
+ ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
+ _ => Colors.Transparent,
+ };
+
+ // Since the blur amount is absolute, we need to scale it down for the preview (which is smaller than full screen).
+ public int EffectiveBackgroundImageBlurAmount => (int)Math.Round(BackgroundImageBlurAmount / 4f);
+
+ public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
+
+ public ImageSource? EffectiveBackgroundImageSource =>
+ ColorizationMode is ColorizationMode.Image
+ && !string.IsNullOrWhiteSpace(BackgroundImagePath)
+ && Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
+ ? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
+ : null;
+
+ public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
+ {
+ _themeService = themeService;
+ _themeService.ThemeChanged += ThemeServiceOnThemeChanged;
+ _settings = settings;
+
+ _uiSettings = new UISettings();
+ _uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
+ UpdateAccentColor(_uiSettings);
+
+ Reapply();
+
+ IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None;
+ }
+
+ private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
+
+ private void UpdateAccentColor(UISettings sender)
+ {
+ _currentSystemAccentColor = sender.GetColorValue(UIColorType.Accent);
+ if (ColorizationMode == ColorizationMode.WindowsAccentColor)
+ {
+ ThemeColor = _currentSystemAccentColor;
+ }
+ }
+
+ private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
+ {
+ _saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
+ }
+
+ private void Save()
+ {
+ SettingsModel.SaveSettings(_settings);
+ _saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
+ }
+
+ private void Reapply()
+ {
+ // Theme services recalculates effective color and opacity based on current settings.
+ EffectiveBackdrop = _themeService.Current.BackdropParameters;
+ OnPropertyChanged(nameof(EffectiveBackdrop));
+ OnPropertyChanged(nameof(EffectiveBackgroundImageBrightness));
+ OnPropertyChanged(nameof(EffectiveBackgroundImageSource));
+ OnPropertyChanged(nameof(EffectiveThemeColor));
+ OnPropertyChanged(nameof(EffectiveBackgroundImageBlurAmount));
+
+ // LOAD BEARING:
+ // We need to cycle through the EffectiveTheme property to force reload of resources.
+ _elementThemeOverride = ElementTheme.Light;
+ OnPropertyChanged(nameof(EffectiveTheme));
+ _elementThemeOverride = ElementTheme.Dark;
+ OnPropertyChanged(nameof(EffectiveTheme));
+ _elementThemeOverride = null;
+ OnPropertyChanged(nameof(EffectiveTheme));
+ }
+
+ [RelayCommand]
+ private void ResetBackgroundImageProperties()
+ {
+ BackgroundImageBrightness = 0;
+ BackgroundImageBlurAmount = 0;
+ BackgroundImageFit = BackgroundImageFit.UniformToFill;
+ BackgroundImageOpacity = 100;
+ ColorIntensity = 0;
+ }
+
+ public void Dispose()
+ {
+ _uiSettings.ColorValuesChanged -= UiSettingsOnColorValuesChanged;
+ _themeService.ThemeChanged -= ThemeServiceOnThemeChanged;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/BackgroundImageFit.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/BackgroundImageFit.cs
new file mode 100644
index 0000000000..52102df30a
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/BackgroundImageFit.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.CmdPal.UI.ViewModels;
+
+public enum BackgroundImageFit
+{
+ Fill,
+ UniformToFill,
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ColorizationMode.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ColorizationMode.cs
new file mode 100644
index 0000000000..57a65f1882
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ColorizationMode.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.CmdPal.UI.ViewModels;
+
+public enum ColorizationMode
+{
+ None,
+ WindowsAccentColor,
+ CustomColor,
+ Image,
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/MainWindowViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/MainWindowViewModel.cs
new file mode 100644
index 0000000000..140811c784
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/MainWindowViewModel.cs
@@ -0,0 +1,70 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Mvvm.ComponentModel;
+using Microsoft.CmdPal.UI.ViewModels.Services;
+using Microsoft.UI.Dispatching;
+using Microsoft.UI.Xaml.Media;
+using Windows.UI;
+
+namespace Microsoft.CmdPal.UI.ViewModels;
+
+public partial class MainWindowViewModel : ObservableObject, IDisposable
+{
+ private readonly IThemeService _themeService;
+ private readonly DispatcherQueue _uiDispatcherQueue = DispatcherQueue.GetForCurrentThread()!;
+
+ [ObservableProperty]
+ public partial ImageSource? BackgroundImageSource { get; private set; }
+
+ [ObservableProperty]
+ public partial Stretch BackgroundImageStretch { get; private set; } = Stretch.Fill;
+
+ [ObservableProperty]
+ public partial double BackgroundImageOpacity { get; private set; }
+
+ [ObservableProperty]
+ public partial Color BackgroundImageTint { get; private set; }
+
+ [ObservableProperty]
+ public partial double BackgroundImageTintIntensity { get; private set; }
+
+ [ObservableProperty]
+ public partial int BackgroundImageBlurAmount { get; private set; }
+
+ [ObservableProperty]
+ public partial double BackgroundImageBrightness { get; private set; }
+
+ [ObservableProperty]
+ public partial bool ShowBackgroundImage { get; private set; }
+
+ public MainWindowViewModel(IThemeService themeService)
+ {
+ _themeService = themeService;
+ _themeService.ThemeChanged += ThemeService_ThemeChanged;
+ }
+
+ private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
+ {
+ _uiDispatcherQueue.TryEnqueue(() =>
+ {
+ BackgroundImageSource = _themeService.Current.BackgroundImageSource;
+ BackgroundImageStretch = _themeService.Current.BackgroundImageStretch;
+ BackgroundImageOpacity = _themeService.Current.BackgroundImageOpacity;
+
+ BackgroundImageBrightness = _themeService.Current.BackgroundBrightness;
+ BackgroundImageTint = _themeService.Current.Tint;
+ BackgroundImageTintIntensity = _themeService.Current.TintIntensity;
+ BackgroundImageBlurAmount = _themeService.Current.BlurAmount;
+
+ ShowBackgroundImage = BackgroundImageSource != null;
+ });
+ }
+
+ public void Dispose()
+ {
+ _themeService.ThemeChanged -= ThemeService_ThemeChanged;
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj
index 6b1b018273..1c85aa939b 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj
@@ -23,11 +23,12 @@
+
compile
-
+
compile
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs
index be9d103b2d..8bc2a42a92 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs
@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
@@ -411,6 +411,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
}
+ ///
+ /// Looks up a localized string similar to Pick background image.
+ ///
+ public static string builtin_settings_appearance_pick_background_image_title {
+ get {
+ return ResourceManager.GetString("builtin_settings_appearance_pick_background_image_title", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to {0} extensions found.
///
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx
index 9a658e38f1..bb7637e133 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx
@@ -239,4 +239,7 @@
{0} extensions installed
+
+ Pick background image
+
\ No newline at end of file
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/AcrylicBackdropParameters.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/AcrylicBackdropParameters.cs
new file mode 100644
index 0000000000..efb7ca1fa1
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/AcrylicBackdropParameters.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Windows.UI;
+
+namespace Microsoft.CmdPal.UI.ViewModels.Services;
+
+public sealed record AcrylicBackdropParameters(Color TintColor, Color FallbackColor, float TintOpacity, float LuminosityOpacity);
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/IThemeService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/IThemeService.cs
new file mode 100644
index 0000000000..546742b8f4
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/IThemeService.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.CmdPal.UI.ViewModels.Services;
+
+///
+/// Provides theme-related values for the Command Palette and notifies listeners about
+/// changes that affect visual appearance (theme, tint, background image, and backdrop).
+///
+///
+/// Implementations are expected to monitor system/app theme changes and raise
+/// accordingly. Consumers should call
+/// once to hook required sources and then query properties/methods for the current visuals.
+///
+public interface IThemeService
+{
+ ///
+ /// Occurs when the effective theme or any visual-affecting setting changes.
+ ///
+ ///
+ /// Triggered for changes such as app theme (light/dark/default), background image,
+ /// tint/accent, or backdrop parameters that would require UI to refresh styling.
+ ///
+ event EventHandler? ThemeChanged;
+
+ ///
+ /// Initializes the theme service and starts listening for theme-related changes.
+ ///
+ ///
+ /// Safe to call once during application startup before consuming the service.
+ ///
+ void Initialize();
+
+ ///
+ /// Gets the current theme settings.
+ ///
+ ThemeSnapshot Current { get; }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ThemeChangedEventArgs.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ThemeChangedEventArgs.cs
new file mode 100644
index 0000000000..96197dc376
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ThemeChangedEventArgs.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.CmdPal.UI.ViewModels.Services;
+
+///
+/// Event arguments for theme-related changes.
+public class ThemeChangedEventArgs : EventArgs;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ThemeSnapshot.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ThemeSnapshot.cs
new file mode 100644
index 0000000000..244fd41fba
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ThemeSnapshot.cs
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Media;
+using Windows.UI;
+
+namespace Microsoft.CmdPal.UI.ViewModels.Services;
+
+///
+/// Represents a snapshot of theme-related visual settings, including accent color, theme preference, and background
+/// image configuration, for use in rendering the Command Palette UI.
+///
+public sealed class ThemeSnapshot
+{
+ ///
+ /// Gets the accent tint color used by the Command Palette visuals.
+ ///
+ public required Color Tint { get; init; }
+
+ ///
+ /// Gets the accent tint color used by the Command Palette visuals.
+ ///
+ public required float TintIntensity { get; init; }
+
+ ///
+ /// Gets the configured application theme preference.
+ ///
+ public required ElementTheme Theme { get; init; }
+
+ ///
+ /// Gets the image source to render as the background, if any.
+ ///
+ ///
+ /// Returns when no background image is configured.
+ ///
+ public required ImageSource? BackgroundImageSource { get; init; }
+
+ ///
+ /// Gets the stretch mode used to lay out the background image.
+ ///
+ public required Stretch BackgroundImageStretch { get; init; }
+
+ ///
+ /// Gets the opacity applied to the background image.
+ ///
+ ///
+ /// A value in the range [0, 1], where 0 is fully transparent and 1 is fully opaque.
+ ///
+ public required double BackgroundImageOpacity { get; init; }
+
+ ///
+ /// Gets the effective acrylic backdrop parameters based on current settings and theme.
+ ///
+ /// The resolved AcrylicBackdropParameters to apply.
+ public required AcrylicBackdropParameters BackdropParameters { get; init; }
+
+ public required int BlurAmount { get; init; }
+
+ public required float BackgroundBrightness { get; init; }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
index dae50b3f3e..e210359f76 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
@@ -11,7 +11,9 @@ using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions.Toolkit;
+using Microsoft.UI;
using Windows.Foundation;
+using Windows.UI;
namespace Microsoft.CmdPal.UI.ViewModels;
@@ -62,6 +64,24 @@ public partial class SettingsModel : ObservableObject
public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; set; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack;
+ public UserTheme Theme { get; set; } = UserTheme.Default;
+
+ public ColorizationMode ColorizationMode { get; set; }
+
+ public Color CustomThemeColor { get; set; } = Colors.Transparent;
+
+ public int CustomThemeColorIntensity { get; set; } = 100;
+
+ public int BackgroundImageOpacity { get; set; } = 20;
+
+ public int BackgroundImageBlurAmount { get; set; }
+
+ public int BackgroundImageBrightness { get; set; }
+
+ public BackgroundImageFit BackgroundImageFit { get; set; }
+
+ public string? BackgroundImagePath { get; set; }
+
// END SETTINGS
///////////////////////////////////////////////////////////////////////////
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
index 586670bff7..6ac9acacc4 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
@@ -4,6 +4,8 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
+using Microsoft.CmdPal.Core.Common.Services;
+using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.Extensions.DependencyInjection;
@@ -29,6 +31,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
+ public AppearanceSettingsViewModel Appearance { get; }
+
public HotkeySettings? Hotkey
{
get => _settings.Hotkey;
@@ -179,6 +183,9 @@ public partial class SettingsViewModel : INotifyPropertyChanged
_settings = settings;
_serviceProvider = serviceProvider;
+ var themeService = serviceProvider.GetRequiredService();
+ Appearance = new AppearanceSettingsViewModel(themeService, _settings);
+
var activeProviders = GetCommandProviders();
var allProviderSettings = _settings.ProviderSettings;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/UserTheme.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/UserTheme.cs
new file mode 100644
index 0000000000..290668f3f5
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/UserTheme.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.CmdPal.UI.ViewModels;
+
+public enum UserTheme
+{
+ Default,
+ Light,
+ Dark,
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml
index f9a9e37ea1..d8d4655291 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml
@@ -4,19 +4,23 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
- xmlns:local="using:Microsoft.CmdPal.UI">
+ xmlns:local="using:Microsoft.CmdPal.UI"
+ xmlns:services="using:Microsoft.CmdPal.UI.Services">
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs
index 53f47286b2..a44682218f 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs
@@ -24,9 +24,11 @@ using Microsoft.CmdPal.Ext.WindowsTerminal;
using Microsoft.CmdPal.Ext.WindowWalker;
using Microsoft.CmdPal.Ext.WinGet;
using Microsoft.CmdPal.UI.Helpers;
+using Microsoft.CmdPal.UI.Services;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
using Microsoft.CmdPal.UI.ViewModels.Models;
+using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerToys.Telemetry;
@@ -112,6 +114,17 @@ public partial class App : Application
// Root services
services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext());
+ AddBuiltInCommands(services);
+
+ AddCoreServices(services);
+
+ AddUIServices(services);
+
+ return services.BuildServiceProvider();
+ }
+
+ private static void AddBuiltInCommands(ServiceCollection services)
+ {
// Built-in Commands. Order matters - this is the order they'll be presented by default.
var allApps = new AllAppsCommandProvider();
var files = new IndexerCommandsProvider();
@@ -154,17 +167,32 @@ public partial class App : Application
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ }
+ private static void AddUIServices(ServiceCollection services)
+ {
// Models
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
var sm = SettingsModel.LoadSettings();
services.AddSingleton(sm);
var state = AppStateModel.LoadState();
services.AddSingleton(state);
- services.AddSingleton();
+
+ // Services
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ services.AddSingleton();
services.AddSingleton();
+
+ services.AddSingleton();
+ services.AddSingleton();
+ }
+
+ private static void AddCoreServices(ServiceCollection services)
+ {
+ // Core services
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
@@ -174,7 +202,5 @@ public partial class App : Application
// ViewModels
services.AddSingleton();
services.AddSingleton();
-
- return services.BuildServiceProvider();
}
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/BlurImageControl.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/BlurImageControl.cs
new file mode 100644
index 0000000000..743e68d690
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/BlurImageControl.cs
@@ -0,0 +1,412 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Numerics;
+using ManagedCommon;
+using Microsoft.Graphics.Canvas.Effects;
+using Microsoft.UI;
+using Microsoft.UI.Composition;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Hosting;
+using Microsoft.UI.Xaml.Media;
+using Windows.UI;
+
+namespace Microsoft.CmdPal.UI.Controls;
+
+internal sealed partial class BlurImageControl : Control
+{
+ private const string ImageSourceParameterName = "ImageSource";
+
+ private const string BrightnessEffectName = "Brightness";
+ private const string BrightnessOverlayEffectName = "BrightnessOverlay";
+ private const string BlurEffectName = "Blur";
+ private const string TintBlendEffectName = "TintBlend";
+ private const string TintEffectName = "Tint";
+
+#pragma warning disable CA1507 // Use nameof to express symbol names ... some of these refer to effect properties that are separate from the class properties
+ private static readonly string BrightnessSource1AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source1Amount");
+ private static readonly string BrightnessSource2AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source2Amount");
+ private static readonly string BrightnessOverlayColorEffectProperty = GetPropertyName(BrightnessOverlayEffectName, "Color");
+ private static readonly string BlurBlurAmountEffectProperty = GetPropertyName(BlurEffectName, "BlurAmount");
+ private static readonly string TintColorEffectProperty = GetPropertyName(TintEffectName, "Color");
+#pragma warning restore CA1507
+
+ private static readonly string[] AnimatableProperties = [
+ BrightnessSource1AmountEffectProperty,
+ BrightnessSource2AmountEffectProperty,
+ BrightnessOverlayColorEffectProperty,
+ BlurBlurAmountEffectProperty,
+ TintColorEffectProperty
+ ];
+
+ public static readonly DependencyProperty ImageSourceProperty =
+ DependencyProperty.Register(
+ nameof(ImageSource),
+ typeof(ImageSource),
+ typeof(BlurImageControl),
+ new PropertyMetadata(null, OnImageChanged));
+
+ public static readonly DependencyProperty ImageStretchProperty =
+ DependencyProperty.Register(
+ nameof(ImageStretch),
+ typeof(Stretch),
+ typeof(BlurImageControl),
+ new PropertyMetadata(Stretch.UniformToFill, OnImageStretchChanged));
+
+ public static readonly DependencyProperty ImageOpacityProperty =
+ DependencyProperty.Register(
+ nameof(ImageOpacity),
+ typeof(double),
+ typeof(BlurImageControl),
+ new PropertyMetadata(1.0, OnOpacityChanged));
+
+ public static readonly DependencyProperty ImageBrightnessProperty =
+ DependencyProperty.Register(
+ nameof(ImageBrightness),
+ typeof(double),
+ typeof(BlurImageControl),
+ new PropertyMetadata(1.0, OnBrightnessChanged));
+
+ public static readonly DependencyProperty BlurAmountProperty =
+ DependencyProperty.Register(
+ nameof(BlurAmount),
+ typeof(double),
+ typeof(BlurImageControl),
+ new PropertyMetadata(0.0, OnBlurAmountChanged));
+
+ public static readonly DependencyProperty TintColorProperty =
+ DependencyProperty.Register(
+ nameof(TintColor),
+ typeof(Color),
+ typeof(BlurImageControl),
+ new PropertyMetadata(Colors.Transparent, OnVisualPropertyChanged));
+
+ public static readonly DependencyProperty TintIntensityProperty =
+ DependencyProperty.Register(
+ nameof(TintIntensity),
+ typeof(double),
+ typeof(BlurImageControl),
+ new PropertyMetadata(0.0, OnVisualPropertyChanged));
+
+ private Compositor? _compositor;
+ private SpriteVisual? _effectVisual;
+ private CompositionEffectBrush? _effectBrush;
+ private CompositionSurfaceBrush? _imageBrush;
+
+ public BlurImageControl()
+ {
+ this.DefaultStyleKey = typeof(BlurImageControl);
+ this.Loaded += OnLoaded;
+ this.SizeChanged += OnSizeChanged;
+ }
+
+ public ImageSource ImageSource
+ {
+ get => (ImageSource)GetValue(ImageSourceProperty);
+ set => SetValue(ImageSourceProperty, value);
+ }
+
+ public Stretch ImageStretch
+ {
+ get => (Stretch)GetValue(ImageStretchProperty);
+ set => SetValue(ImageStretchProperty, value);
+ }
+
+ public double ImageOpacity
+ {
+ get => (double)GetValue(ImageOpacityProperty);
+ set => SetValue(ImageOpacityProperty, value);
+ }
+
+ public double ImageBrightness
+ {
+ get => (double)GetValue(ImageBrightnessProperty);
+ set => SetValue(ImageBrightnessProperty, Math.Clamp(value, -1, 1));
+ }
+
+ public double BlurAmount
+ {
+ get => (double)GetValue(BlurAmountProperty);
+ set => SetValue(BlurAmountProperty, value);
+ }
+
+ public Color TintColor
+ {
+ get => (Color)GetValue(TintColorProperty);
+ set => SetValue(TintColorProperty, value);
+ }
+
+ public double TintIntensity
+ {
+ get => (double)GetValue(TintIntensityProperty);
+ set => SetValue(TintIntensityProperty, value);
+ }
+
+ private static void OnImageStretchChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is BlurImageControl control && control._imageBrush != null)
+ {
+ control._imageBrush.Stretch = ConvertStretch((Stretch)e.NewValue);
+ }
+ }
+
+ private static void OnVisualPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is BlurImageControl control && control._compositor != null)
+ {
+ control.UpdateEffect();
+ }
+ }
+
+ private static void OnOpacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is BlurImageControl control && control._effectVisual != null)
+ {
+ control._effectVisual.Opacity = (float)(double)e.NewValue;
+ }
+ }
+
+ private static void OnBlurAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is BlurImageControl control && control._effectBrush != null)
+ {
+ control.UpdateEffect();
+ }
+ }
+
+ private static void OnBrightnessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is BlurImageControl control && control._effectBrush != null)
+ {
+ control.UpdateEffect();
+ }
+ }
+
+ private void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ InitializeComposition();
+ }
+
+ private void OnSizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ if (_effectVisual != null)
+ {
+ _effectVisual.Size = new Vector2(
+ (float)Math.Max(1, e.NewSize.Width),
+ (float)Math.Max(1, e.NewSize.Height));
+ }
+ }
+
+ private static void OnImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is not BlurImageControl control)
+ {
+ return;
+ }
+
+ control.EnsureEffect(force: true);
+ control.UpdateEffect();
+ }
+
+ private void InitializeComposition()
+ {
+ var visual = ElementCompositionPreview.GetElementVisual(this);
+ _compositor = visual.Compositor;
+
+ _effectVisual = _compositor.CreateSpriteVisual();
+ _effectVisual.Size = new Vector2(
+ (float)Math.Max(1, ActualWidth),
+ (float)Math.Max(1, ActualHeight));
+ _effectVisual.Opacity = (float)ImageOpacity;
+
+ ElementCompositionPreview.SetElementChildVisual(this, _effectVisual);
+
+ UpdateEffect();
+ }
+
+ private void EnsureEffect(bool force = false)
+ {
+ if (_compositor is null)
+ {
+ return;
+ }
+
+ if (_effectBrush is not null && !force)
+ {
+ return;
+ }
+
+ var imageSource = new CompositionEffectSourceParameter(ImageSourceParameterName);
+
+ // 1) Brightness via ArithmeticCompositeEffect
+ // We blend between the original image and either black or white,
+ // depending on whether we want to darken or brighten. BrightnessEffect isn't supported
+ // in the composition graph.
+ var brightnessEffect = new ArithmeticCompositeEffect
+ {
+ Name = BrightnessEffectName,
+ Source1 = imageSource, // original image
+ Source2 = new ColorSourceEffect
+ {
+ Name = BrightnessOverlayEffectName,
+ Color = Colors.Black, // we'll swap black/white via properties
+ },
+
+ MultiplyAmount = 0.0f,
+ Source1Amount = 1.0f, // original
+ Source2Amount = 0.0f, // overlay
+ Offset = 0.0f,
+ };
+
+ // 2) Blur
+ var blurEffect = new GaussianBlurEffect
+ {
+ Name = BlurEffectName,
+ BlurAmount = 0.0f,
+ BorderMode = EffectBorderMode.Hard,
+ Optimization = EffectOptimization.Balanced,
+ Source = brightnessEffect,
+ };
+
+ // 3) Tint (always in the chain; intensity via alpha)
+ var tintEffect = new BlendEffect
+ {
+ Name = TintBlendEffectName,
+ Background = blurEffect,
+ Foreground = new ColorSourceEffect
+ {
+ Name = TintEffectName,
+ Color = Colors.Transparent,
+ },
+ Mode = BlendEffectMode.Multiply,
+ };
+
+ var effectFactory = _compositor.CreateEffectFactory(tintEffect, AnimatableProperties);
+
+ _effectBrush?.Dispose();
+ _effectBrush = effectFactory.CreateBrush();
+
+ // Set initial source
+ if (ImageSource is not null)
+ {
+ _imageBrush ??= _compositor.CreateSurfaceBrush();
+ LoadImageAsync(ImageSource);
+ _effectBrush.SetSourceParameter(ImageSourceParameterName, _imageBrush);
+ }
+ else
+ {
+ _effectBrush.SetSourceParameter(ImageSourceParameterName, _compositor.CreateBackdropBrush());
+ }
+
+ if (_effectVisual is not null)
+ {
+ _effectVisual.Brush = _effectBrush;
+ }
+ }
+
+ private void UpdateEffect()
+ {
+ if (_compositor is null)
+ {
+ return;
+ }
+
+ EnsureEffect();
+ if (_effectBrush is null)
+ {
+ return;
+ }
+
+ var props = _effectBrush.Properties;
+
+ // Brightness
+ var b = (float)Math.Clamp(ImageBrightness, -1.0, 1.0);
+
+ float source1Amount;
+ float source2Amount;
+ Color overlayColor;
+
+ if (b >= 0)
+ {
+ // Brighten: blend towards white
+ overlayColor = Colors.White;
+ source1Amount = 1.0f - b; // original image contribution
+ source2Amount = b; // white overlay contribution
+ }
+ else
+ {
+ // Darken: blend towards black
+ overlayColor = Colors.Black;
+ var t = -b; // 0..1
+ source1Amount = 1.0f - t; // original image
+ source2Amount = t; // black overlay
+ }
+
+ props.InsertScalar(BrightnessSource1AmountEffectProperty, source1Amount);
+ props.InsertScalar(BrightnessSource2AmountEffectProperty, source2Amount);
+ props.InsertColor(BrightnessOverlayColorEffectProperty, overlayColor);
+
+ // Blur
+ props.InsertScalar(BlurBlurAmountEffectProperty, (float)BlurAmount);
+
+ // Tint
+ var tintColor = TintColor;
+ var clampedIntensity = (float)Math.Clamp(TintIntensity, 0.0, 1.0);
+
+ var adjustedColor = Color.FromArgb(
+ (byte)(clampedIntensity * 255),
+ tintColor.R,
+ tintColor.G,
+ tintColor.B);
+
+ props.InsertColor(TintColorEffectProperty, adjustedColor);
+ }
+
+ private void LoadImageAsync(ImageSource imageSource)
+ {
+ try
+ {
+ if (imageSource is Microsoft.UI.Xaml.Media.Imaging.BitmapImage bitmapImage)
+ {
+ _imageBrush ??= _compositor?.CreateSurfaceBrush();
+ if (_imageBrush is null)
+ {
+ return;
+ }
+
+ var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource);
+ loadedSurface.LoadCompleted += (_, _) =>
+ {
+ if (_imageBrush is not null)
+ {
+ _imageBrush.Surface = loadedSurface;
+ _imageBrush.Stretch = ConvertStretch(ImageStretch);
+ _imageBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.Linear;
+ }
+ };
+
+ _effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to load image for BlurImageControl: {0}", ex);
+ }
+ }
+
+ private static CompositionStretch ConvertStretch(Stretch stretch)
+ {
+ return stretch switch
+ {
+ Stretch.None => CompositionStretch.None,
+ Stretch.Fill => CompositionStretch.Fill,
+ Stretch.Uniform => CompositionStretch.Uniform,
+ Stretch.UniformToFill => CompositionStretch.UniformToFill,
+ _ => CompositionStretch.UniformToFill,
+ };
+ }
+
+ private static string GetPropertyName(string effectName, string propertyName) => $"{effectName}.{propertyName}";
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPalette.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPalette.xaml
new file mode 100644
index 0000000000..105010bbd2
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPalette.xaml
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPalette.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPalette.xaml.cs
new file mode 100644
index 0000000000..7267e894fa
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPalette.xaml.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.ObjectModel;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Windows.UI;
+
+namespace Microsoft.CmdPal.UI.Controls;
+
+public sealed partial class ColorPalette : UserControl
+{
+ public static readonly DependencyProperty PaletteColorsProperty = DependencyProperty.Register(nameof(PaletteColors), typeof(ObservableCollection), typeof(ColorPalette), null!)!;
+
+ public static readonly DependencyProperty CustomPaletteColumnCountProperty = DependencyProperty.Register(nameof(CustomPaletteColumnCount), typeof(int), typeof(ColorPalette), new PropertyMetadata(10))!;
+
+ public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ColorPalette), new PropertyMetadata(null!))!;
+
+ public event EventHandler? SelectedColorChanged;
+
+ private Color? _selectedColor;
+
+ public Color? SelectedColor
+ {
+ get => _selectedColor;
+
+ set
+ {
+ if (_selectedColor != value)
+ {
+ _selectedColor = value;
+ if (value is not null)
+ {
+ SetValue(SelectedColorProperty, value);
+ }
+ else
+ {
+ ClearValue(SelectedColorProperty);
+ }
+ }
+ }
+ }
+
+ public ObservableCollection PaletteColors
+ {
+ get => (ObservableCollection)GetValue(PaletteColorsProperty)!;
+ set => SetValue(PaletteColorsProperty, value);
+ }
+
+ public int CustomPaletteColumnCount
+ {
+ get => (int)GetValue(CustomPaletteColumnCountProperty);
+ set => SetValue(CustomPaletteColumnCountProperty, value);
+ }
+
+ public ColorPalette()
+ {
+ PaletteColors = [];
+ InitializeComponent();
+ }
+
+ private void ListViewBase_OnItemClick(object sender, ItemClickEventArgs e)
+ {
+ if (e.ClickedItem is Color color)
+ {
+ SelectedColor = color;
+ SelectedColorChanged?.Invoke(this, color);
+ }
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPickerButton.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPickerButton.xaml
new file mode 100644
index 0000000000..92a556f7a7
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPickerButton.xaml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPickerButton.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPickerButton.xaml.cs
new file mode 100644
index 0000000000..ff82fffd4e
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPickerButton.xaml.cs
@@ -0,0 +1,146 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.ObjectModel;
+using ManagedCommon;
+using Microsoft.UI;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Windows.UI;
+
+namespace Microsoft.CmdPal.UI.Controls;
+
+public sealed partial class ColorPickerButton : UserControl
+{
+ public static readonly DependencyProperty PaletteColorsProperty = DependencyProperty.Register(nameof(PaletteColors), typeof(ObservableCollection), typeof(ColorPickerButton), new PropertyMetadata(new ObservableCollection()))!;
+
+ public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ColorPickerButton), new PropertyMetadata(Colors.Black))!;
+
+ public static readonly DependencyProperty IsAlphaEnabledProperty = DependencyProperty.Register(nameof(IsAlphaEnabled), typeof(bool), typeof(ColorPickerButton), new PropertyMetadata(defaultValue: false))!;
+
+ public static readonly DependencyProperty IsValueEditorEnabledProperty = DependencyProperty.Register(nameof(IsValueEditorEnabled), typeof(bool), typeof(ColorPickerButton), new PropertyMetadata(false))!;
+
+ public static readonly DependencyProperty HasSelectedColorProperty = DependencyProperty.Register(nameof(HasSelectedColor), typeof(bool), typeof(ColorPickerButton), new PropertyMetadata(false))!;
+
+ private Color _selectedColor;
+
+ public Color SelectedColor
+ {
+ get
+ {
+ return _selectedColor;
+ }
+
+ set
+ {
+ if (_selectedColor != value)
+ {
+ _selectedColor = value;
+ SetValue(SelectedColorProperty, value);
+ HasSelectedColor = true;
+ }
+ }
+ }
+
+ public bool HasSelectedColor
+ {
+ get { return (bool)GetValue(HasSelectedColorProperty); }
+ set { SetValue(HasSelectedColorProperty, value); }
+ }
+
+ public bool IsAlphaEnabled
+ {
+ get => (bool)GetValue(IsAlphaEnabledProperty);
+ set => SetValue(IsAlphaEnabledProperty, value);
+ }
+
+ public bool IsValueEditorEnabled
+ {
+ get { return (bool)GetValue(IsValueEditorEnabledProperty); }
+ set { SetValue(IsValueEditorEnabledProperty, value); }
+ }
+
+ public ObservableCollection PaletteColors
+ {
+ get { return (ObservableCollection)GetValue(PaletteColorsProperty); }
+ set { SetValue(PaletteColorsProperty, value); }
+ }
+
+ public ColorPickerButton()
+ {
+ this.InitializeComponent();
+
+ IsEnabledChanged -= ColorPickerButton_IsEnabledChanged;
+ SetEnabledState();
+ IsEnabledChanged += ColorPickerButton_IsEnabledChanged;
+ }
+
+ private void ColorPickerButton_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
+ {
+ SetEnabledState();
+ }
+
+ private void SetEnabledState()
+ {
+ if (this.IsEnabled)
+ {
+ ColorPreviewBorder.Opacity = 1;
+ }
+ else
+ {
+ ColorPreviewBorder.Opacity = 0.2;
+ }
+ }
+
+ private void ColorPalette_OnSelectedColorChanged(object? sender, Color? e)
+ {
+ if (e.HasValue)
+ {
+ HasSelectedColor = true;
+ SelectedColor = e.Value;
+ }
+ }
+
+ private void FlyoutBase_OnOpened(object? sender, object e)
+ {
+ if (sender is not Flyout flyout || (flyout.Content as FrameworkElement)?.Parent is not FlyoutPresenter flyoutPresenter)
+ {
+ return;
+ }
+
+ FlyoutRoot!.UpdateLayout();
+ flyoutPresenter.UpdateLayout();
+
+ // Logger.LogInfo($"FlyoutBase_OnOpened: {flyoutPresenter}, {FlyoutRoot!.ActualWidth}");
+ flyoutPresenter.MaxWidth = FlyoutRoot!.ActualWidth;
+ flyoutPresenter.MinWidth = 660;
+ flyoutPresenter.Width = FlyoutRoot!.ActualWidth;
+ }
+
+ private void FlyoutRoot_OnSizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ if ((ColorPickerFlyout!.Content as FrameworkElement)?.Parent is not FlyoutPresenter flyoutPresenter)
+ {
+ return;
+ }
+
+ FlyoutRoot!.UpdateLayout();
+ flyoutPresenter.UpdateLayout();
+
+ flyoutPresenter.MaxWidth = FlyoutRoot!.ActualWidth;
+ flyoutPresenter.MinWidth = 660;
+ flyoutPresenter.Width = FlyoutRoot!.ActualWidth;
+ }
+
+ private Thickness ToDropDownPadding(bool hasColor)
+ {
+ return hasColor ? new Thickness(3, 3, 8, 3) : new Thickness(8, 4, 8, 4);
+ }
+
+ private void ResetButton_Click(object sender, RoutedEventArgs e)
+ {
+ HasSelectedColor = false;
+ ColorPickerFlyout?.Hide();
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandPalettePreview.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandPalettePreview.xaml
new file mode 100644
index 0000000000..a30d1fafdf
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandPalettePreview.xaml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandPalettePreview.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandPalettePreview.xaml.cs
new file mode 100644
index 0000000000..96cd5d6aac
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandPalettePreview.xaml.cs
@@ -0,0 +1,123 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CmdPal.UI.ViewModels;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
+using Windows.UI;
+
+namespace Microsoft.CmdPal.UI.Controls;
+
+public sealed partial class CommandPalettePreview : UserControl
+{
+ public static readonly DependencyProperty PreviewBackgroundOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundOpacity), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d));
+
+ public static readonly DependencyProperty PreviewBackgroundColorProperty = DependencyProperty.Register(nameof(PreviewBackgroundColor), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color)));
+
+ public static readonly DependencyProperty PreviewBackgroundImageSourceProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageSource), typeof(ImageSource), typeof(CommandPalettePreview), new PropertyMetadata(null, PropertyChangedCallback));
+
+ public static readonly DependencyProperty PreviewBackgroundImageOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageOpacity), typeof(int), typeof(CommandPalettePreview), new PropertyMetadata(0));
+
+ public static readonly DependencyProperty PreviewBackgroundImageFitProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageFit), typeof(BackgroundImageFit), typeof(CommandPalettePreview), new PropertyMetadata(default(BackgroundImageFit)));
+
+ public static readonly DependencyProperty PreviewBackgroundImageBrightnessProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageBrightness), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d));
+
+ public static readonly DependencyProperty PreviewBackgroundImageBlurAmountProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageBlurAmount), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d));
+
+ public static readonly DependencyProperty PreviewBackgroundImageTintProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageTint), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color)));
+
+ public static readonly DependencyProperty PreviewBackgroundImageTintIntensityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageTintIntensity), typeof(int), typeof(CommandPalettePreview), new PropertyMetadata(0));
+
+ public static readonly DependencyProperty ShowBackgroundImageProperty = DependencyProperty.Register(nameof(ShowBackgroundImage), typeof(Visibility), typeof(CommandPalettePreview), new PropertyMetadata(Visibility.Collapsed));
+
+ public BackgroundImageFit PreviewBackgroundImageFit
+ {
+ get { return (BackgroundImageFit)GetValue(PreviewBackgroundImageFitProperty); }
+ set { SetValue(PreviewBackgroundImageFitProperty, value); }
+ }
+
+ public double PreviewBackgroundOpacity
+ {
+ get { return (double)GetValue(PreviewBackgroundOpacityProperty); }
+ set { SetValue(PreviewBackgroundOpacityProperty, value); }
+ }
+
+ public Color PreviewBackgroundColor
+ {
+ get { return (Color)GetValue(PreviewBackgroundColorProperty); }
+ set { SetValue(PreviewBackgroundColorProperty, value); }
+ }
+
+ public ImageSource PreviewBackgroundImageSource
+ {
+ get { return (ImageSource)GetValue(PreviewBackgroundImageSourceProperty); }
+ set { SetValue(PreviewBackgroundImageSourceProperty, value); }
+ }
+
+ public int PreviewBackgroundImageOpacity
+ {
+ get { return (int)GetValue(PreviewBackgroundImageOpacityProperty); }
+ set { SetValue(PreviewBackgroundImageOpacityProperty, value); }
+ }
+
+ public double PreviewBackgroundImageBrightness
+ {
+ get => (double)GetValue(PreviewBackgroundImageBrightnessProperty);
+ set => SetValue(PreviewBackgroundImageBrightnessProperty, value);
+ }
+
+ public double PreviewBackgroundImageBlurAmount
+ {
+ get => (double)GetValue(PreviewBackgroundImageBlurAmountProperty);
+ set => SetValue(PreviewBackgroundImageBlurAmountProperty, value);
+ }
+
+ public Color PreviewBackgroundImageTint
+ {
+ get => (Color)GetValue(PreviewBackgroundImageTintProperty);
+ set => SetValue(PreviewBackgroundImageTintProperty, value);
+ }
+
+ public int PreviewBackgroundImageTintIntensity
+ {
+ get => (int)GetValue(PreviewBackgroundImageTintIntensityProperty);
+ set => SetValue(PreviewBackgroundImageTintIntensityProperty, value);
+ }
+
+ public Visibility ShowBackgroundImage
+ {
+ get => (Visibility)GetValue(ShowBackgroundImageProperty);
+ set => SetValue(ShowBackgroundImageProperty, value);
+ }
+
+ public CommandPalettePreview()
+ {
+ InitializeComponent();
+ }
+
+ private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is not CommandPalettePreview preview)
+ {
+ return;
+ }
+
+ preview.ShowBackgroundImage = e.NewValue is ImageSource ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ private double ToOpacity(int value) => value / 100.0;
+
+ private double ToTintIntensity(int value) => value / 100.0;
+
+ private Stretch ToStretch(BackgroundImageFit fit)
+ {
+ return fit switch
+ {
+ BackgroundImageFit.Fill => Stretch.Fill,
+ BackgroundImageFit.UniformToFill => Stretch.UniformToFill,
+ _ => Stretch.None,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScreenPreview.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScreenPreview.xaml
new file mode 100644
index 0000000000..58c4e890a6
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScreenPreview.xaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScreenPreview.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScreenPreview.xaml.cs
new file mode 100644
index 0000000000..828fa76c74
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScreenPreview.xaml.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CmdPal.UI.Helpers;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Markup;
+using Microsoft.UI.Xaml.Media;
+
+namespace Microsoft.CmdPal.UI.Controls;
+
+[ContentProperty(Name = nameof(PreviewContent))]
+public sealed partial class ScreenPreview : UserControl
+{
+ public static readonly DependencyProperty PreviewContentProperty =
+ DependencyProperty.Register(nameof(PreviewContent), typeof(object), typeof(ScreenPreview), new PropertyMetadata(null!))!;
+
+ public object PreviewContent
+ {
+ get => GetValue(PreviewContentProperty)!;
+ set => SetValue(PreviewContentProperty, value);
+ }
+
+ public ScreenPreview()
+ {
+ InitializeComponent();
+
+ var wallpaperHelper = new WallpaperHelper();
+ WallpaperImage!.Source = wallpaperHelper.GetWallpaperImage()!;
+ ScreenBorder!.Background = new SolidColorBrush(wallpaperHelper.GetWallpaperColor());
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml
index 80eb1a3ad6..d248c24f89 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml
@@ -4,9 +4,8 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cmdpalUi="using:Microsoft.CmdPal.UI"
- xmlns:converters="using:CommunityToolkit.WinUI.Converters"
- xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:h="using:Microsoft.CmdPal.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
@@ -22,6 +21,7 @@
MinHeight="32"
VerticalAlignment="Stretch"
VerticalContentAlignment="Stretch"
+ h:TextBoxCaretColor.SyncWithForeground="True"
AutomationProperties.AutomationId="MainSearchBox"
KeyDown="FilterBox_KeyDown"
PlaceholderText="{x:Bind CurrentPageViewModel.PlaceholderText, Converter={StaticResource PlaceholderTextConverter}, Mode=OneWay}"
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContrastBrushConverter.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContrastBrushConverter.cs
new file mode 100644
index 0000000000..5f54682aaf
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContrastBrushConverter.cs
@@ -0,0 +1,121 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.UI;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.UI.Xaml.Media;
+using Windows.UI;
+
+namespace Microsoft.CmdPal.UI.Converters;
+
+///
+/// Gets a color, either black or white, depending on the brightness of the supplied color.
+///
+public sealed partial class ContrastBrushConverter : IValueConverter
+{
+ ///
+ /// Gets or sets the alpha channel threshold below which a default color is used instead of black/white.
+ ///
+ public byte AlphaThreshold { get; set; } = 128;
+
+ ///
+ public object Convert(
+ object value,
+ Type targetType,
+ object parameter,
+ string language)
+ {
+ Color comparisonColor;
+ Color? defaultColor = null;
+
+ // Get the changing color to compare against
+ if (value is Color valueColor)
+ {
+ comparisonColor = valueColor;
+ }
+ else if (value is SolidColorBrush valueBrush)
+ {
+ comparisonColor = valueBrush.Color;
+ }
+ else
+ {
+ // Invalid color value provided
+ return DependencyProperty.UnsetValue;
+ }
+
+ // Get the default color when transparency is high
+ if (parameter is Color parameterColor)
+ {
+ defaultColor = parameterColor;
+ }
+ else if (parameter is SolidColorBrush parameterBrush)
+ {
+ defaultColor = parameterBrush.Color;
+ }
+
+ if (comparisonColor.A < AlphaThreshold &&
+ defaultColor.HasValue)
+ {
+ // If the transparency is less than 50 %, just use the default brush
+ // This can commonly be something like the TextControlForeground brush
+ return new SolidColorBrush(defaultColor.Value);
+ }
+ else
+ {
+ // Chose a white/black brush based on contrast to the base color
+ return UseLightContrastColor(comparisonColor)
+ ? new SolidColorBrush(Colors.White)
+ : new SolidColorBrush(Colors.Black);
+ }
+ }
+
+ ///
+ public object ConvertBack(
+ object value,
+ Type targetType,
+ object parameter,
+ string language)
+ {
+ return DependencyProperty.UnsetValue;
+ }
+
+ ///
+ /// Determines whether a light or dark contrast color should be used with the given displayed color.
+ ///
+ ///
+ /// This code is using the WinUI algorithm.
+ ///
+ private bool UseLightContrastColor(Color displayedColor)
+ {
+ // The selection ellipse should be light if and only if the chosen color
+ // contrasts more with black than it does with white.
+ // To find how much something contrasts with white, we use the equation
+ // for relative luminance, which is given by
+ //
+ // L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg
+ //
+ // where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise }
+ //
+ // If L is closer to 1, then the color is closer to white; if it is closer to 0,
+ // then the color is closer to black. This is based on the fact that the human
+ // eye perceives green to be much brighter than red, which in turn is perceived to be
+ // brighter than blue.
+ //
+ // If the third dimension is value, then we won't be updating the spectrum's displayed colors,
+ // so in that case we should use a value of 1 when considering the backdrop
+ // for the selection ellipse.
+ var rg = displayedColor.R <= 10
+ ? displayedColor.R / 3294.0
+ : Math.Pow((displayedColor.R / 269.0) + 0.0513, 2.4);
+ var gg = displayedColor.G <= 10
+ ? displayedColor.G / 3294.0
+ : Math.Pow((displayedColor.G / 269.0) + 0.0513, 2.4);
+ var bg = displayedColor.B <= 10
+ ? displayedColor.B / 3294.0
+ : Math.Pow((displayedColor.B / 269.0) + 0.0513, 2.4);
+
+ return (0.2126 * rg) + (0.7152 * gg) + (0.0722 * bg) <= 0.5;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs
index 012e8dc789..f00c230da5 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs
@@ -10,6 +10,8 @@ internal static class BindTransformers
{
public static bool Negate(bool value) => !value;
+ public static Visibility NegateVisibility(Visibility value) => value == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
+
public static Visibility EmptyToCollapsed(string? input)
=> string.IsNullOrEmpty(input) ? Visibility.Collapsed : Visibility.Visible;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/ColorExtensions.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/ColorExtensions.cs
new file mode 100644
index 0000000000..2492f7f7c9
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/ColorExtensions.cs
@@ -0,0 +1,132 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI.Helpers;
+using Windows.UI;
+
+namespace Microsoft.CmdPal.UI.Helpers;
+
+///
+/// Extension methods for .
+///
+internal static class ColorExtensions
+{
+ /// Input color.
+ public static double CalculateBrightness(this Color color)
+ {
+ return color.ToHsv().V;
+ }
+
+ ///
+ /// Allows to change the brightness by a factor based on the HSV color space.
+ ///
+ /// Input color.
+ /// The brightness adjustment factor, ranging from -1 to 1.
+ /// Updated color.
+ public static Color UpdateBrightness(this Color color, double brightnessFactor)
+ {
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(brightnessFactor, 1);
+ ArgumentOutOfRangeException.ThrowIfLessThan(brightnessFactor, -1);
+
+ var hsvColor = color.ToHsv();
+ return ColorHelper.FromHsv(hsvColor.H, hsvColor.S, Math.Clamp(hsvColor.V + brightnessFactor, 0, 1), hsvColor.A);
+ }
+
+ ///
+ /// Updates the color by adjusting brightness, saturation, and luminance factors.
+ ///
+ /// Input color.
+ /// The brightness adjustment factor, ranging from -1 to 1.
+ /// The saturation adjustment factor, ranging from -1 to 1. Defaults to 0.
+ /// The luminance adjustment factor, ranging from -1 to 1. Defaults to 0.
+ /// Updated color.
+ public static Color Update(this Color color, double brightnessFactor, double saturationFactor = 0, double luminanceFactor = 0)
+ {
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(brightnessFactor, 1);
+ ArgumentOutOfRangeException.ThrowIfLessThan(brightnessFactor, -1);
+
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(saturationFactor, 1);
+ ArgumentOutOfRangeException.ThrowIfLessThan(saturationFactor, -1);
+
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(luminanceFactor, 1);
+ ArgumentOutOfRangeException.ThrowIfLessThan(luminanceFactor, -1);
+
+ var hsv = color.ToHsv();
+
+ var rgb = ColorHelper.FromHsv(
+ hsv.H,
+ Clamp01(hsv.S + saturationFactor),
+ Clamp01(hsv.V + brightnessFactor));
+
+ if (luminanceFactor == 0)
+ {
+ return rgb;
+ }
+
+ var hsl = rgb.ToHsl();
+ var lightness = Clamp01(hsl.L + luminanceFactor);
+ return ColorHelper.FromHsl(hsl.H, hsl.S, lightness);
+ }
+
+ ///
+ /// Linearly interpolates between two colors in HSV space.
+ /// Hue is blended along the shortest arc on the color wheel (wrap-aware).
+ /// Saturation, Value, and Alpha are blended linearly.
+ ///
+ /// Start color.
+ /// End color.
+ /// Interpolation factor in [0,1].
+ /// Interpolated color.
+ public static Color LerpHsv(this Color a, Color b, double t)
+ {
+ t = Clamp01(t);
+
+ // Convert to HSV
+ var hslA = a.ToHsv();
+ var hslB = b.ToHsv();
+
+ var h1 = hslA.H;
+ var h2 = hslB.H;
+
+ // Handle near-gray hues (undefined hue) by inheriting the other's hue
+ const double satEps = 1e-4f;
+ if (hslA.S < satEps && hslB.S >= satEps)
+ {
+ h1 = h2;
+ }
+ else if (hslB.S < satEps && hslA.S >= satEps)
+ {
+ h2 = h1;
+ }
+
+ return ColorHelper.FromHsv(
+ hue: LerpHueDegrees(h1, h2, t),
+ saturation: Lerp(hslA.S, hslB.S, t),
+ value: Lerp(hslA.V, hslB.V, t),
+ alpha: (byte)Math.Round(Lerp(hslA.A, hslB.A, t)));
+ }
+
+ private static double LerpHueDegrees(double a, double b, double t)
+ {
+ a = Mod360(a);
+ b = Mod360(b);
+ var delta = ((b - a + 540f) % 360f) - 180f;
+ return Mod360(a + (delta * t));
+ }
+
+ private static double Mod360(double angle)
+ {
+ angle %= 360f;
+ if (angle < 0f)
+ {
+ angle += 360f;
+ }
+
+ return angle;
+ }
+
+ private static double Lerp(double a, double b, double t) => a + ((b - a) * t);
+
+ private static double Clamp01(double x) => Math.Clamp(x, 0, 1);
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TextBoxCaretColor.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TextBoxCaretColor.cs
new file mode 100644
index 0000000000..f5103b9efc
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TextBoxCaretColor.cs
@@ -0,0 +1,173 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using CommunityToolkit.WinUI;
+using Microsoft.UI;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
+using Rectangle = Microsoft.UI.Xaml.Shapes.Rectangle;
+
+namespace Microsoft.CmdPal.UI.Helpers;
+
+///
+/// Attached property to color internal caret/overlay rectangles inside a TextBox
+/// so they follow the TextBox's actual Foreground brush.
+///
+public static class TextBoxCaretColor
+{
+ public static readonly DependencyProperty SyncWithForegroundProperty =
+ DependencyProperty.RegisterAttached("SyncWithForeground", typeof(bool), typeof(TextBoxCaretColor), new PropertyMetadata(false, OnSyncCaretRectanglesChanged))!;
+
+ private static readonly ConditionalWeakTable States = [];
+
+ public static void SetSyncWithForeground(DependencyObject obj, bool value)
+ {
+ obj.SetValue(SyncWithForegroundProperty, value);
+ }
+
+ public static bool GetSyncWithForeground(DependencyObject obj)
+ {
+ return (bool)obj.GetValue(SyncWithForegroundProperty);
+ }
+
+ private static void OnSyncCaretRectanglesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is not TextBox tb)
+ {
+ return;
+ }
+
+ if ((bool)e.NewValue)
+ {
+ Attach(tb);
+ }
+ else
+ {
+ Detach(tb);
+ }
+ }
+
+ private static void Attach(TextBox tb)
+ {
+ if (States.TryGetValue(tb, out var st) && st.IsHooked)
+ {
+ return;
+ }
+
+ st ??= new State();
+ st.IsHooked = true;
+ States.Remove(tb);
+ States.Add(tb, st);
+
+ tb.Loaded += TbOnLoaded;
+ tb.Unloaded += TbOnUnloaded;
+ tb.GotFocus += TbOnGotFocus;
+
+ st.ForegroundToken = tb.RegisterPropertyChangedCallback(Control.ForegroundProperty!, (_, _) => Apply(tb));
+
+ if (tb.IsLoaded)
+ {
+ Apply(tb);
+ }
+ }
+
+ private static void Detach(TextBox tb)
+ {
+ if (!States.TryGetValue(tb, out var st))
+ {
+ return;
+ }
+
+ tb.Loaded -= TbOnLoaded;
+ tb.Unloaded -= TbOnUnloaded;
+ tb.GotFocus -= TbOnGotFocus;
+
+ if (st.ForegroundToken != 0)
+ {
+ tb.UnregisterPropertyChangedCallback(Control.ForegroundProperty!, st.ForegroundToken);
+ st.ForegroundToken = 0;
+ }
+
+ st.IsHooked = false;
+ }
+
+ private static void TbOnLoaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is TextBox tb)
+ {
+ Apply(tb);
+ }
+ }
+
+ private static void TbOnUnloaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is TextBox tb)
+ {
+ Detach(tb);
+ }
+ }
+
+ private static void TbOnGotFocus(object sender, RoutedEventArgs e)
+ {
+ if (sender is TextBox tb)
+ {
+ Apply(tb);
+ }
+ }
+
+ private static void Apply(TextBox tb)
+ {
+ try
+ {
+ ApplyCore(tb);
+ }
+ catch (COMException)
+ {
+ // ignore
+ }
+ }
+
+ private static void ApplyCore(TextBox tb)
+ {
+ // Ensure template is realized
+ tb.ApplyTemplate();
+
+ // Find the internal ScrollContentPresenter within the TextBox template
+ var scp = tb.FindDescendant(s => s.Name == "ScrollContentPresenter");
+ if (scp is null)
+ {
+ return;
+ }
+
+ var brush = tb.Foreground; // use the actual current foreground brush
+ if (brush == null)
+ {
+ brush = new SolidColorBrush(Colors.Black);
+ }
+
+ foreach (var rect in scp.FindDescendants().OfType())
+ {
+ try
+ {
+ rect.Fill = brush;
+ rect.CompositeMode = ElementCompositeMode.SourceOver;
+ rect.Opacity = 0.9;
+ }
+ catch
+ {
+ // best-effort; some rectangles might be template-owned
+ }
+ }
+ }
+
+ private sealed class State
+ {
+ public long ForegroundToken { get; set; }
+
+ public bool IsHooked { get; set; }
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WallpaperHelper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WallpaperHelper.cs
new file mode 100644
index 0000000000..9772d33b1d
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WallpaperHelper.cs
@@ -0,0 +1,178 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.Marshalling;
+using ManagedCommon;
+using ManagedCsWin32;
+using Microsoft.UI;
+using Microsoft.UI.Xaml.Media.Imaging;
+using Windows.UI;
+
+namespace Microsoft.CmdPal.UI.Helpers;
+
+///
+/// Lightweight helper to access wallpaper information.
+///
+internal sealed partial class WallpaperHelper
+{
+ private readonly IDesktopWallpaper? _desktopWallpaper;
+
+ public WallpaperHelper()
+ {
+ try
+ {
+ var desktopWallpaper = ComHelper.CreateComInstance(
+ ref Unsafe.AsRef(in CLSID.DesktopWallpaper),
+ CLSCTX.ALL);
+
+ _desktopWallpaper = desktopWallpaper;
+ }
+ catch (Exception ex)
+ {
+ // If COM initialization fails, keep helper usable with safe fallbacks
+ Logger.LogError("Failed to initialize DesktopWallpaper COM interface", ex);
+ _desktopWallpaper = null;
+ }
+ }
+
+ private string? GetWallpaperPathForFirstMonitor()
+ {
+ try
+ {
+ if (_desktopWallpaper is null)
+ {
+ return null;
+ }
+
+ _desktopWallpaper.GetMonitorDevicePathCount(out var monitorCount);
+
+ for (uint i = 0; monitorCount != 0 && i < monitorCount; i++)
+ {
+ _desktopWallpaper.GetMonitorDevicePathAt(i, out var monitorId);
+ if (string.IsNullOrEmpty(monitorId))
+ {
+ continue;
+ }
+
+ _desktopWallpaper.GetWallpaper(monitorId, out var wallpaperPath);
+
+ if (!string.IsNullOrWhiteSpace(wallpaperPath) && File.Exists(wallpaperPath))
+ {
+ return wallpaperPath;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to query wallpaper path", ex);
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets the wallpaper background color.
+ ///
+ /// The wallpaper background color, or black if it cannot be determined.
+ public Color GetWallpaperColor()
+ {
+ try
+ {
+ if (_desktopWallpaper is null)
+ {
+ return Colors.Black;
+ }
+
+ _desktopWallpaper.GetBackgroundColor(out var colorref);
+ var r = (byte)(colorref.Value & 0x000000FF);
+ var g = (byte)((colorref.Value & 0x0000FF00) >> 8);
+ var b = (byte)((colorref.Value & 0x00FF0000) >> 16);
+ return Color.FromArgb(255, r, g, b);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to load wallpaper color", ex);
+ return Colors.Black;
+ }
+ }
+
+ ///
+ /// Gets the wallpaper image for the primary monitor.
+ ///
+ /// The wallpaper image, or null if it cannot be determined.
+ public BitmapImage? GetWallpaperImage()
+ {
+ try
+ {
+ var path = GetWallpaperPathForFirstMonitor();
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return null;
+ }
+
+ var image = new BitmapImage();
+ using var stream = File.OpenRead(path);
+ var randomAccessStream = stream.AsRandomAccessStream();
+ if (randomAccessStream == null)
+ {
+ Logger.LogError("Failed to convert file stream to RandomAccessStream for wallpaper image.");
+ return null;
+ }
+
+ image.SetSource(randomAccessStream);
+ return image;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to load wallpaper image", ex);
+ return null;
+ }
+ }
+
+ // blittable type for COM interop
+ [StructLayout(LayoutKind.Sequential)]
+ internal readonly partial struct COLORREF
+ {
+ internal readonly uint Value;
+ }
+
+ // blittable type for COM interop
+ [StructLayout(LayoutKind.Sequential)]
+ internal readonly partial struct RECT
+ {
+ internal readonly int Left;
+ internal readonly int Top;
+ internal readonly int Right;
+ internal readonly int Bottom;
+ }
+
+ // COM interface for IDesktopWallpaper, GeneratedComInterface to be AOT compatible
+ [GeneratedComInterface]
+ [Guid("B92B56A9-8B55-4E14-9A89-0199BBB6F93B")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ internal partial interface IDesktopWallpaper
+ {
+ void SetWallpaper(
+ [MarshalAs(UnmanagedType.LPWStr)] string? monitorId,
+ [MarshalAs(UnmanagedType.LPWStr)] string wallpaper);
+
+ void GetWallpaper(
+ [MarshalAs(UnmanagedType.LPWStr)] string? monitorId,
+ [MarshalAs(UnmanagedType.LPWStr)] out string wallpaper);
+
+ void GetMonitorDevicePathAt(uint monitorIndex, [MarshalAs(UnmanagedType.LPWStr)] out string monitorId);
+
+ void GetMonitorDevicePathCount(out uint count);
+
+ void GetMonitorRECT([MarshalAs(UnmanagedType.LPWStr)] string? monitorId, out RECT rect);
+
+ void SetBackgroundColor(COLORREF color);
+
+ void GetBackgroundColor(out COLORREF color);
+
+ // Other methods omitted for brevity
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml
index c0c0ab811f..32329e17a0 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml
@@ -2,6 +2,7 @@
x:Class="Microsoft.CmdPal.UI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pages="using:Microsoft.CmdPal.UI.Pages"
@@ -15,6 +16,21 @@
Closed="MainWindow_Closed"
mc:Ignorable="d">
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
index 1655626714..d9acdb48d9 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
@@ -15,8 +15,10 @@ using Microsoft.CmdPal.UI.Controls;
using Microsoft.CmdPal.UI.Events;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages;
+using Microsoft.CmdPal.UI.Services;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
+using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI;
@@ -66,7 +68,10 @@ public sealed partial class MainWindow : WindowEx,
private readonly KeyboardListener _keyboardListener;
private readonly LocalKeyboardListener _localKeyboardListener;
private readonly HiddenOwnerWindowBehavior _hiddenOwnerBehavior = new();
+ private readonly IThemeService _themeService;
+ private readonly WindowThemeSynchronizer _windowThemeSynchronizer;
private bool _ignoreHotKeyWhenFullScreen = true;
+ private bool _themeServiceInitialized;
private DesktopAcrylicController? _acrylicController;
private SystemBackdropConfiguration? _configurationSource;
@@ -74,13 +79,21 @@ public sealed partial class MainWindow : WindowEx,
private WindowPosition _currentWindowPosition = new();
+ private MainWindowViewModel ViewModel { get; }
+
public MainWindow()
{
InitializeComponent();
+ ViewModel = App.Current.Services.GetService()!;
+
_autoGoHomeTimer = new DispatcherTimer();
_autoGoHomeTimer.Tick += OnAutoGoHomeTimerOnTick;
+ _themeService = App.Current.Services.GetRequiredService();
+ _themeService.ThemeChanged += ThemeServiceOnThemeChanged;
+ _windowThemeSynchronizer = new WindowThemeSynchronizer(_themeService, this);
+
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
unsafe
@@ -88,6 +101,8 @@ public sealed partial class MainWindow : WindowEx,
CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value);
}
+ SetAcrylic();
+
_hiddenOwnerBehavior.ShowInTaskbar(this, Debugger.IsAttached);
_keyboardListener = new KeyboardListener();
@@ -100,8 +115,6 @@ public sealed partial class MainWindow : WindowEx,
RestoreWindowPosition();
UpdateWindowPositionInMemory();
- SetAcrylic();
-
WeakReferenceMessenger.Default.Register(this);
WeakReferenceMessenger.Default.Register(this);
WeakReferenceMessenger.Default.Register(this);
@@ -156,6 +169,11 @@ public sealed partial class MainWindow : WindowEx,
WeakReferenceMessenger.Default.Send(new GoHomeMessage(WithAnimation: false, FocusSearch: false));
}
+ private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
+ {
+ UpdateAcrylic();
+ }
+
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
{
if (e.Key == VirtualKey.GoBack)
@@ -247,8 +265,6 @@ public sealed partial class MainWindow : WindowEx,
_autoGoHomeTimer.Interval = _autoGoHomeInterval;
}
- // We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material
- // other Shell surfaces are using, this cannot be set in XAML however.
private void SetAcrylic()
{
if (DesktopAcrylicController.IsSupported())
@@ -265,41 +281,32 @@ public sealed partial class MainWindow : WindowEx,
private void UpdateAcrylic()
{
- if (_acrylicController != null)
+ try
{
- _acrylicController.RemoveAllSystemBackdropTargets();
- _acrylicController.Dispose();
- }
-
- _acrylicController = GetAcrylicConfig(Content);
-
- // Enable the system backdrop.
- // Note: Be sure to have "using WinRT;" to support the Window.As<...>() call.
- _acrylicController.AddSystemBackdropTarget(this.As());
- _acrylicController.SetSystemBackdropConfiguration(_configurationSource);
- }
-
- private static DesktopAcrylicController GetAcrylicConfig(UIElement content)
- {
- var feContent = content as FrameworkElement;
-
- return feContent?.ActualTheme == ElementTheme.Light
- ? new DesktopAcrylicController()
+ if (_acrylicController != null)
{
- Kind = DesktopAcrylicKind.Thin,
- TintColor = Color.FromArgb(255, 243, 243, 243),
- LuminosityOpacity = 0.90f,
- TintOpacity = 0.0f,
- FallbackColor = Color.FromArgb(255, 238, 238, 238),
+ _acrylicController.RemoveAllSystemBackdropTargets();
+ _acrylicController.Dispose();
}
- : new DesktopAcrylicController()
+
+ var backdrop = _themeService.Current.BackdropParameters;
+ _acrylicController = new DesktopAcrylicController
{
- Kind = DesktopAcrylicKind.Thin,
- TintColor = Color.FromArgb(255, 32, 32, 32),
- LuminosityOpacity = 0.96f,
- TintOpacity = 0.5f,
- FallbackColor = Color.FromArgb(255, 28, 28, 28),
+ TintColor = backdrop.TintColor,
+ TintOpacity = backdrop.TintOpacity,
+ FallbackColor = backdrop.FallbackColor,
+ LuminosityOpacity = backdrop.LuminosityOpacity,
};
+
+ // Enable the system backdrop.
+ // Note: Be sure to have "using WinRT;" to support the Window.As<...>() call.
+ _acrylicController.AddSystemBackdropTarget(this.As());
+ _acrylicController.SetSystemBackdropConfiguration(_configurationSource);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to update backdrop", ex);
+ }
}
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
@@ -711,6 +718,19 @@ public sealed partial class MainWindow : WindowEx,
internal void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
+ if (!_themeServiceInitialized && args.WindowActivationState != WindowActivationState.Deactivated)
+ {
+ try
+ {
+ _themeService.Initialize();
+ _themeServiceInitialized = true;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to initialize ThemeService", ex);
+ }
+ }
+
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
// Save the current window position before hiding the window
@@ -1004,6 +1024,7 @@ public sealed partial class MainWindow : WindowEx,
public void Dispose()
{
_localKeyboardListener.Dispose();
+ _windowThemeSynchronizer.Dispose();
DisposeAcrylic();
}
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
index 8397ffc767..54961a5828 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
@@ -68,8 +68,11 @@
+
+
+
@@ -78,10 +81,12 @@
+
+
@@ -93,6 +98,7 @@
+
@@ -207,6 +213,39 @@
+
+
+ MSBuild:Compile
+
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+
+ Designer
+
+
+ MSBuild:Compile
+
+
+
+
+
+ MSBuild:Compile
+
+
+
MSBuild:Compile
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
index fe1a29dd97..04b4ca6c16 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
@@ -11,7 +11,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:h="using:Microsoft.CmdPal.UI.Helpers"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
- xmlns:labToolkit="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock"
xmlns:markdownImageProviders="using:Microsoft.CmdPal.UI.Helpers.MarkdownImageProviders"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
@@ -177,7 +176,7 @@
-
+
@@ -190,7 +189,7 @@
Padding="0,12,0,12"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
- BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
+ BorderBrush="{ThemeResource CmdPal.TopBarBorderBrush}"
BorderThickness="0,0,0,1">
@@ -390,7 +389,7 @@
HorizontalAlignment="Stretch"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
- BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
+ BorderBrush="{ThemeResource CmdPal.DividerStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{StaticResource ControlCornerRadius}"
Visibility="Collapsed">
@@ -518,7 +517,7 @@
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ColorfulThemeProvider.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ColorfulThemeProvider.cs
new file mode 100644
index 0000000000..fe6c8e48e0
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ColorfulThemeProvider.cs
@@ -0,0 +1,207 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI.Helpers;
+using Microsoft.CmdPal.UI.Helpers;
+using Microsoft.CmdPal.UI.ViewModels.Services;
+using Microsoft.UI.Xaml;
+using Windows.UI;
+using Windows.UI.ViewManagement;
+
+namespace Microsoft.CmdPal.UI.Services;
+
+///
+/// Provides theme appropriate for colorful (accented) appearance.
+///
+internal sealed class ColorfulThemeProvider : IThemeProvider
+{
+ // Fluent dark: #202020
+ private static readonly Color DarkBaseColor = Color.FromArgb(255, 32, 32, 32);
+
+ // Fluent light: #F3F3F3
+ private static readonly Color LightBaseColor = Color.FromArgb(255, 243, 243, 243);
+
+ private readonly UISettings _uiSettings;
+
+ public string ThemeKey => "colorful";
+
+ public string ResourcePath => "ms-appx:///Styles/Theme.Colorful.xaml";
+
+ public ColorfulThemeProvider(UISettings uiSettings)
+ {
+ ArgumentNullException.ThrowIfNull(uiSettings);
+ _uiSettings = uiSettings;
+ }
+
+ public AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context)
+ {
+ var isLight = context.Theme == ElementTheme.Light ||
+ (context.Theme == ElementTheme.Default &&
+ _uiSettings.GetColorValue(UIColorType.Background).R > 128);
+
+ var baseColor = isLight ? LightBaseColor : DarkBaseColor;
+
+ // Windows is warping the hue of accent colors and running it through some curves to produce their accent shades.
+ // This will attempt to mimic that behavior.
+ var accentShades = AccentShades.Compute(context.Tint.LerpHsv(WindowsAccentHueWarpTransform.Transform(context.Tint), 0.5f));
+ var blended = isLight ? accentShades.Light3 : accentShades.Dark2;
+ var colorIntensityUser = (context.ColorIntensity ?? 100) / 100f;
+
+ // For light theme, we want to reduce intensity a bit, and also we need to keep the color fairly light,
+ // to avoid issues with text box caret.
+ var colorIntensity = isLight ? 0.6f * colorIntensityUser : colorIntensityUser;
+ var effectiveBgColor = ColorBlender.Blend(baseColor, blended, colorIntensity);
+
+ return new AcrylicBackdropParameters(effectiveBgColor, effectiveBgColor, 0.8f, 0.8f);
+ }
+
+ private static class ColorBlender
+ {
+ ///
+ /// Blends a semitransparent tint color over an opaque base color using alpha compositing.
+ ///
+ /// The opaque base color (background)
+ /// The semitransparent tint color (foreground)
+ /// The intensity of the tint (0.0 - 1.0)
+ /// The resulting blended color
+ public static Color Blend(Color baseColor, Color tintColor, float intensity)
+ {
+ // Normalize alpha to 0.0 - 1.0 range
+ intensity = Math.Clamp(intensity, 0f, 1f);
+
+ // Alpha compositing formula: result = tint * alpha + base * (1 - alpha)
+ var r = (byte)((tintColor.R * intensity) + (baseColor.R * (1 - intensity)));
+ var g = (byte)((tintColor.G * intensity) + (baseColor.G * (1 - intensity)));
+ var b = (byte)((tintColor.B * intensity) + (baseColor.B * (1 - intensity)));
+
+ // Result is fully opaque since base is opaque
+ return Color.FromArgb(255, r, g, b);
+ }
+ }
+
+ private static class WindowsAccentHueWarpTransform
+ {
+ private static readonly (double HIn, double HOut)[] HueMap =
+ [
+ (0, 0),
+ (10, 1),
+ (20, 6),
+ (30, 10),
+ (40, 14),
+ (50, 19),
+ (60, 36),
+ (70, 94),
+ (80, 112),
+ (90, 120),
+ (100, 120),
+ (110, 120),
+ (120, 120),
+ (130, 120),
+ (140, 120),
+ (150, 125),
+ (160, 135),
+ (170, 142),
+ (180, 178),
+ (190, 205),
+ (200, 220),
+ (210, 229),
+ (220, 237),
+ (230, 241),
+ (240, 243),
+ (250, 244),
+ (260, 245),
+ (270, 248),
+ (280, 252),
+ (290, 276),
+ (300, 293),
+ (310, 313),
+ (320, 330),
+ (330, 349),
+ (340, 353),
+ (350, 357)
+ ];
+
+ public static Color Transform(Color input, Options? opt = null)
+ {
+ opt ??= new Options();
+ var hsv = input.ToHsv();
+ return ColorHelper.FromHsv(
+ RemapHueLut(hsv.H),
+ Clamp01(Math.Pow(hsv.S, opt.SaturationGamma) * opt.SaturationGain),
+ Clamp01((opt.ValueScaleA * hsv.V) + opt.ValueBiasB),
+ input.A);
+ }
+
+ // Hue LUT remap (piecewise-linear with cyclic wrap)
+ private static double RemapHueLut(double hDeg)
+ {
+ // Normalize to [0,360)
+ hDeg = Mod(hDeg, 360.0);
+
+ // Handle wrap-around case: hDeg is between last entry (350°) and 360°
+ var last = HueMap[^1];
+ var first = HueMap[0];
+ if (hDeg >= last.HIn)
+ {
+ // Interpolate between last entry and first entry (wrapped by 360°)
+ var t = (hDeg - last.HIn) / (first.HIn + 360.0 - last.HIn + 1e-12);
+ var ho = Lerp(last.HOut, first.HOut + 360.0, t);
+ return Mod(ho, 360.0);
+ }
+
+ // Find segment [i, i+1] where HueMap[i].HIn <= hDeg < HueMap[i+1].HIn
+ for (var i = 0; i < HueMap.Length - 1; i++)
+ {
+ var a = HueMap[i];
+ var b = HueMap[i + 1];
+
+ if (hDeg >= a.HIn && hDeg < b.HIn)
+ {
+ var t = (hDeg - a.HIn) / (b.HIn - a.HIn + 1e-12);
+ return Lerp(a.HOut, b.HOut, t);
+ }
+ }
+
+ // Fallback (shouldn't happen)
+ return hDeg;
+ }
+
+ private static double Lerp(double a, double b, double t) => a + ((b - a) * t);
+
+ private static double Mod(double x, double m) => ((x % m) + m) % m;
+
+ private static double Clamp01(double x) => x < 0 ? 0 : (x > 1 ? 1 : x);
+
+ public sealed class Options
+ {
+ // Saturation boost (1.0 = no change). Typical: 1.3–1.8
+ public double SaturationGain { get; init; } = 1.0;
+
+ // Optional saturation gamma (1.0 = linear). <1.0 raises low S a bit; >1.0 preserves low S.
+ public double SaturationGamma { get; init; } = 1.0;
+
+ // Value (V) remap: V' = a*V + b (tone curve; clamp applied)
+ // Example that lifts blacks & compresses whites slightly: a=0.50, b=0.08
+ public double ValueScaleA { get; init; } = 0.6;
+
+ public double ValueBiasB { get; init; } = 0.01;
+ }
+ }
+
+ private static class AccentShades
+ {
+ public static (Color Light3, Color Light2, Color Light1, Color Dark1, Color Dark2, Color Dark3) Compute(Color accent)
+ {
+ var light1 = accent.Update(brightnessFactor: 0.15, saturationFactor: -0.12);
+ var light2 = accent.Update(brightnessFactor: 0.30, saturationFactor: -0.24);
+ var light3 = accent.Update(brightnessFactor: 0.45, saturationFactor: -0.36);
+
+ var dark1 = accent.UpdateBrightness(brightnessFactor: -0.05f);
+ var dark2 = accent.UpdateBrightness(brightnessFactor: -0.01f);
+ var dark3 = accent.UpdateBrightness(brightnessFactor: -0.015f);
+
+ return (light3, light2, light1, dark1, dark2, dark3);
+ }
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/IThemeProvider.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/IThemeProvider.cs
new file mode 100644
index 0000000000..a9411c3656
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/IThemeProvider.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CmdPal.Core.Common.Services;
+using Microsoft.CmdPal.UI.ViewModels.Services;
+
+namespace Microsoft.CmdPal.UI.Services;
+
+///
+/// Provides theme identification, resource path resolution, and creation of acrylic
+/// backdrop parameters based on the current .
+///
+///
+/// Implementations should expose a stable and a valid XAML resource
+/// dictionary path via . The
+/// method computes
+/// using the supplied theme context.
+///
+internal interface IThemeProvider
+{
+ ///
+ /// Gets the unique key identifying this theme provider.
+ ///
+ string ThemeKey { get; }
+
+ ///
+ /// Gets the resource dictionary path for this theme.
+ ///
+ string ResourcePath { get; }
+
+ ///
+ /// Creates acrylic backdrop parameters based on the provided theme context.
+ ///
+ /// The current theme context, including theme, tint, and optional background details.
+ /// The computed for the backdrop.
+ AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context);
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/MutableOverridesDictionary.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/MutableOverridesDictionary.cs
new file mode 100644
index 0000000000..8177326259
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/MutableOverridesDictionary.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.UI.Xaml;
+
+namespace Microsoft.CmdPal.UI.Services;
+
+///
+/// Dedicated ResourceDictionary for dynamic overrides that win over base theme resources. Since
+/// we can't use a key or name to identify the dictionary in Application resources, we use a dedicated type.
+///
+internal sealed partial class MutableOverridesDictionary : ResourceDictionary;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/NormalThemeProvider.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/NormalThemeProvider.cs
new file mode 100644
index 0000000000..c393894346
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/NormalThemeProvider.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CmdPal.UI.ViewModels.Services;
+using Microsoft.UI.Xaml;
+using Windows.UI;
+using Windows.UI.ViewManagement;
+
+namespace Microsoft.CmdPal.UI.Services;
+
+///
+/// Provides theme resources and acrylic backdrop parameters matching the default Command Palette theme.
+///
+internal sealed class NormalThemeProvider : IThemeProvider
+{
+ private static readonly Color DarkBaseColor = Color.FromArgb(255, 32, 32, 32);
+ private static readonly Color LightBaseColor = Color.FromArgb(255, 243, 243, 243);
+ private readonly UISettings _uiSettings;
+
+ public NormalThemeProvider(UISettings uiSettings)
+ {
+ ArgumentNullException.ThrowIfNull(uiSettings);
+ _uiSettings = uiSettings;
+ }
+
+ public string ThemeKey => "normal";
+
+ public string ResourcePath => "ms-appx:///Styles/Theme.Normal.xaml";
+
+ public AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context)
+ {
+ var isLight = context.Theme == ElementTheme.Light ||
+ (context.Theme == ElementTheme.Default &&
+ _uiSettings.GetColorValue(UIColorType.Background).R > 128);
+
+ return new AcrylicBackdropParameters(
+ TintColor: isLight ? LightBaseColor : DarkBaseColor,
+ FallbackColor: isLight ? LightBaseColor : DarkBaseColor,
+ TintOpacity: 0.5f,
+ LuminosityOpacity: isLight ? 0.9f : 0.96f);
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ResourceSwapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ResourceSwapper.cs
new file mode 100644
index 0000000000..6d0a6f01dd
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ResourceSwapper.cs
@@ -0,0 +1,332 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.UI.Xaml;
+
+namespace Microsoft.CmdPal.UI.Services;
+
+///
+/// Simple theme switcher that swaps application ResourceDictionaries at runtime.
+/// Can also operate in event-only mode for consumers to apply resources themselves.
+/// Exposes a dedicated override dictionary that stays merged and is cleared on theme changes.
+///
+internal sealed partial class ResourceSwapper
+{
+ private readonly Lock _resourceSwapGate = new();
+ private readonly Dictionary _themeUris = new(StringComparer.OrdinalIgnoreCase);
+ private ResourceDictionary? _activeDictionary;
+ private string? _currentThemeName;
+ private Uri? _currentThemeUri;
+
+ private ResourceDictionary? _overrideDictionary;
+
+ ///
+ /// Raised after a theme has been activated.
+ ///
+ public event EventHandler? ResourcesSwapped;
+
+ ///
+ /// Gets or sets a value indicating whether when true (default) ResourceSwapper updates Application.Current.Resources. When false, it only raises ResourcesSwapped.
+ ///
+ public bool ApplyToAppResources { get; set; } = true;
+
+ ///
+ /// Gets name of the currently selected theme (if any).
+ ///
+ public string? CurrentThemeName
+ {
+ get
+ {
+ lock (_resourceSwapGate)
+ {
+ return _currentThemeName;
+ }
+ }
+ }
+
+ ///
+ /// Initializes ResourceSwapper by checking Application resources for an already merged theme dictionary.
+ ///
+ public void Initialize()
+ {
+ // Find merged dictionary in Application resources that matches a registered theme by URI
+ // This allows ResourceSwapper to pick up an initial theme set in XAML
+ var app = Application.Current;
+ var resourcesMergedDictionaries = app?.Resources?.MergedDictionaries;
+ if (resourcesMergedDictionaries == null)
+ {
+ return;
+ }
+
+ foreach (var dict in resourcesMergedDictionaries)
+ {
+ var uri = dict.Source;
+ if (uri is null)
+ {
+ continue;
+ }
+
+ var name = GetNameForUri(uri);
+ if (name is null)
+ {
+ continue;
+ }
+
+ lock (_resourceSwapGate)
+ {
+ _currentThemeName = name;
+ _currentThemeUri = uri;
+ _activeDictionary = dict;
+ }
+
+ break;
+ }
+ }
+
+ ///
+ /// Gets uri of the currently selected theme dictionary (if any).
+ ///
+ public Uri? CurrentThemeUri
+ {
+ get
+ {
+ lock (_resourceSwapGate)
+ {
+ return _currentThemeUri;
+ }
+ }
+ }
+
+ public static ResourceDictionary GetOverrideDictionary(bool clear = false)
+ {
+ var app = Application.Current ?? throw new InvalidOperationException("App is null");
+
+ if (app.Resources == null)
+ {
+ throw new InvalidOperationException("Application.Resources is null");
+ }
+
+ // (Re)locate the slot – Hot Reload may rebuild Application.Resources.
+ var slot = app.Resources!.MergedDictionaries!
+ .OfType()
+ .FirstOrDefault();
+
+ if (slot is null)
+ {
+ // If the slot vanished (Hot Reload), create it again at the end so it wins precedence.
+ slot = new MutableOverridesDictionary();
+ app.Resources.MergedDictionaries!.Add(slot);
+ }
+
+ // Ensure the slot has exactly one child RD we can swap safely.
+ if (slot.MergedDictionaries!.Count == 0)
+ {
+ slot.MergedDictionaries.Add(new ResourceDictionary());
+ }
+ else if (slot.MergedDictionaries.Count > 1)
+ {
+ // Normalize to a single child to keep semantics predictable.
+ var keep = slot.MergedDictionaries[^1];
+ slot.MergedDictionaries.Clear();
+ slot.MergedDictionaries.Add(keep);
+ }
+
+ if (clear)
+ {
+ // Swap the child dictionary instead of Clear() to avoid reentrancy issues.
+ var fresh = new ResourceDictionary();
+ slot.MergedDictionaries[0] = fresh;
+ return fresh;
+ }
+
+ return slot.MergedDictionaries[0]!;
+ }
+
+ ///
+ /// Registers a theme name mapped to a XAML ResourceDictionary URI (e.g. ms-appx:///Themes/Red.xaml)
+ ///
+ public void RegisterTheme(string name, Uri dictionaryUri)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentException("Theme name is required", nameof(name));
+ }
+
+ lock (_resourceSwapGate)
+ {
+ _themeUris[name] = dictionaryUri ?? throw new ArgumentNullException(nameof(dictionaryUri));
+ }
+ }
+
+ ///
+ /// Registers a theme with a string URI.
+ ///
+ public void RegisterTheme(string name, string dictionaryUri)
+ {
+ ArgumentNullException.ThrowIfNull(dictionaryUri);
+ RegisterTheme(name, new Uri(dictionaryUri));
+ }
+
+ ///
+ /// Removes a previously registered theme.
+ ///
+ public bool UnregisterTheme(string name)
+ {
+ lock (_resourceSwapGate)
+ {
+ return _themeUris.Remove(name);
+ }
+ }
+
+ ///
+ /// Gets the names of all registered themes.
+ ///
+ public IEnumerable GetRegisteredThemes()
+ {
+ lock (_resourceSwapGate)
+ {
+ // return a copy to avoid external mutation
+ return new List(_themeUris.Keys);
+ }
+ }
+
+ ///
+ /// Activates a theme by name. The dictionary for the given name must be registered first.
+ ///
+ public void ActivateTheme(string theme)
+ {
+ if (string.IsNullOrWhiteSpace(theme))
+ {
+ throw new ArgumentException("Theme name is required", nameof(theme));
+ }
+
+ Uri uri;
+ lock (_resourceSwapGate)
+ {
+ if (!_themeUris.TryGetValue(theme, out uri!))
+ {
+ throw new KeyNotFoundException($"Theme '{theme}' is not registered.");
+ }
+ }
+
+ ActivateThemeInternal(theme, uri);
+ }
+
+ ///
+ /// Tries to activate a theme by name without throwing.
+ ///
+ public bool TryActivateTheme(string theme)
+ {
+ if (string.IsNullOrWhiteSpace(theme))
+ {
+ return false;
+ }
+
+ Uri uri;
+ lock (_resourceSwapGate)
+ {
+ if (!_themeUris.TryGetValue(theme, out uri!))
+ {
+ return false;
+ }
+ }
+
+ ActivateThemeInternal(theme, uri);
+ return true;
+ }
+
+ ///
+ /// Activates a theme by URI to a ResourceDictionary.
+ ///
+ public void ActivateTheme(Uri dictionaryUri)
+ {
+ ArgumentNullException.ThrowIfNull(dictionaryUri);
+
+ ActivateThemeInternal(GetNameForUri(dictionaryUri), dictionaryUri);
+ }
+
+ ///
+ /// Clears the currently active theme ResourceDictionary. Also clears the override dictionary.
+ ///
+ public void ClearActiveTheme()
+ {
+ lock (_resourceSwapGate)
+ {
+ var app = Application.Current;
+ if (app is null)
+ {
+ return;
+ }
+
+ if (_activeDictionary is not null && ApplyToAppResources)
+ {
+ _ = app.Resources.MergedDictionaries.Remove(_activeDictionary);
+ _activeDictionary = null;
+ }
+
+ // Clear overrides but keep the override dictionary merged for future updates
+ _overrideDictionary?.Clear();
+
+ _currentThemeName = null;
+ _currentThemeUri = null;
+ }
+ }
+
+ private void ActivateThemeInternal(string? name, Uri dictionaryUri)
+ {
+ lock (_resourceSwapGate)
+ {
+ _currentThemeName = name;
+ _currentThemeUri = dictionaryUri;
+ }
+
+ if (ApplyToAppResources)
+ {
+ ActivateThemeCore(dictionaryUri);
+ }
+
+ OnResourcesSwapped(new(name, dictionaryUri));
+ }
+
+ private void ActivateThemeCore(Uri dictionaryUri)
+ {
+ var app = Application.Current ?? throw new InvalidOperationException("Application.Current is null");
+
+ // Remove previously applied base theme dictionary
+ if (_activeDictionary is not null)
+ {
+ _ = app.Resources.MergedDictionaries.Remove(_activeDictionary);
+ _activeDictionary = null;
+ }
+
+ // Load and merge the new base theme dictionary
+ var newDict = new ResourceDictionary { Source = dictionaryUri };
+ app.Resources.MergedDictionaries.Add(newDict);
+ _activeDictionary = newDict;
+
+ // Ensure override dictionary exists and is merged last, then clear it to avoid leaking stale overrides
+ _overrideDictionary = GetOverrideDictionary(clear: true);
+ }
+
+ private string? GetNameForUri(Uri dictionaryUri)
+ {
+ lock (_resourceSwapGate)
+ {
+ foreach (var (key, value) in _themeUris)
+ {
+ if (Uri.Compare(value, dictionaryUri, UriComponents.AbsoluteUri, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ return key;
+ }
+ }
+
+ return null;
+ }
+ }
+
+ private void OnResourcesSwapped(ResourcesSwappedEventArgs e)
+ {
+ ResourcesSwapped?.Invoke(this, e);
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ResourcesSwappedEventArgs.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ResourcesSwappedEventArgs.cs
new file mode 100644
index 0000000000..0a5cc15de6
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ResourcesSwappedEventArgs.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.CmdPal.UI.Services;
+
+public sealed class ResourcesSwappedEventArgs(string? name, Uri dictionaryUri) : EventArgs
+{
+ public string? Name { get; } = name;
+
+ public Uri DictionaryUri { get; } = dictionaryUri ?? throw new ArgumentNullException(nameof(dictionaryUri));
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ThemeContext.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ThemeContext.cs
new file mode 100644
index 0000000000..67432c8748
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ThemeContext.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Media;
+using Windows.UI;
+
+namespace Microsoft.CmdPal.UI.Services;
+
+internal sealed record ThemeContext
+{
+ public ElementTheme Theme { get; init; }
+
+ public Color Tint { get; init; }
+
+ public ImageSource? BackgroundImageSource { get; init; }
+
+ public Stretch BackgroundImageStretch { get; init; }
+
+ public double BackgroundImageOpacity { get; init; }
+
+ public int? ColorIntensity { get; init; }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ThemeService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ThemeService.cs
new file mode 100644
index 0000000000..65fbfb24d7
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ThemeService.cs
@@ -0,0 +1,261 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI;
+using ManagedCommon;
+using Microsoft.CmdPal.UI.Helpers;
+using Microsoft.CmdPal.UI.ViewModels;
+using Microsoft.CmdPal.UI.ViewModels.Services;
+using Microsoft.UI;
+using Microsoft.UI.Dispatching;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Media.Imaging;
+using Windows.UI.ViewManagement;
+using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
+
+namespace Microsoft.CmdPal.UI.Services;
+
+///
+/// ThemeService is a hub that translates user settings and system preferences into concrete
+/// theme resources and notifies listeners of changes.
+///
+internal sealed partial class ThemeService : IThemeService, IDisposable
+{
+ private static readonly TimeSpan ReloadDebounceInterval = TimeSpan.FromMilliseconds(500);
+
+ private readonly UISettings _uiSettings;
+ private readonly SettingsModel _settings;
+ private readonly ResourceSwapper _resourceSwapper;
+ private readonly NormalThemeProvider _normalThemeProvider;
+ private readonly ColorfulThemeProvider _colorfulThemeProvider;
+
+ private DispatcherQueue? _dispatcherQueue;
+ private DispatcherQueueTimer? _dispatcherQueueTimer;
+ private bool _isInitialized;
+ private bool _disposed;
+ private InternalThemeState _currentState;
+
+ public event EventHandler? ThemeChanged;
+
+ public ThemeSnapshot Current => Volatile.Read(ref _currentState).Snapshot;
+
+ ///
+ /// Initializes the theme service. Must be called after the application window is activated and on UI thread.
+ ///
+ public void Initialize()
+ {
+ if (_isInitialized)
+ {
+ return;
+ }
+
+ _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
+ if (_dispatcherQueue is null)
+ {
+ throw new InvalidOperationException("Failed to get DispatcherQueue for the current thread. Ensure Initialize is called on the UI thread after window activation.");
+ }
+
+ _dispatcherQueueTimer = _dispatcherQueue.CreateTimer();
+
+ _resourceSwapper.Initialize();
+ _isInitialized = true;
+ Reload();
+ }
+
+ private void Reload()
+ {
+ if (!_isInitialized)
+ {
+ return;
+ }
+
+ // provider selection
+ var intensity = Math.Clamp(_settings.CustomThemeColorIntensity, 0, 100);
+ IThemeProvider provider = intensity > 0 && _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image
+ ? _colorfulThemeProvider
+ : _normalThemeProvider;
+
+ // Calculate values
+ var tint = _settings.ColorizationMode switch
+ {
+ ColorizationMode.CustomColor => _settings.CustomThemeColor,
+ ColorizationMode.WindowsAccentColor => _uiSettings.GetColorValue(UIColorType.Accent),
+ ColorizationMode.Image => _settings.CustomThemeColor,
+ _ => Colors.Transparent,
+ };
+ var effectiveTheme = GetElementTheme((ElementTheme)_settings.Theme);
+ var imageSource = _settings.ColorizationMode == ColorizationMode.Image
+ ? LoadImageSafe(_settings.BackgroundImagePath)
+ : null;
+ var stretch = _settings.BackgroundImageFit switch
+ {
+ BackgroundImageFit.Fill => Stretch.Fill,
+ _ => Stretch.UniformToFill,
+ };
+ var opacity = Math.Clamp(_settings.BackgroundImageOpacity, 0, 100) / 100.0;
+
+ // create context and offload to actual theme provider
+ var context = new ThemeContext
+ {
+ Tint = tint,
+ ColorIntensity = intensity,
+ Theme = effectiveTheme,
+ BackgroundImageSource = imageSource,
+ BackgroundImageStretch = stretch,
+ BackgroundImageOpacity = opacity,
+ };
+ var backdrop = provider.GetAcrylicBackdrop(context);
+ var blur = _settings.BackgroundImageBlurAmount;
+ var brightness = _settings.BackgroundImageBrightness;
+
+ // Create public snapshot (no provider!)
+ var snapshot = new ThemeSnapshot
+ {
+ Tint = tint,
+ TintIntensity = intensity / 100f,
+ Theme = effectiveTheme,
+ BackgroundImageSource = imageSource,
+ BackgroundImageStretch = stretch,
+ BackgroundImageOpacity = opacity,
+ BackdropParameters = backdrop,
+ BlurAmount = blur,
+ BackgroundBrightness = brightness / 100f,
+ };
+
+ // Bundle with provider for internal use
+ var newState = new InternalThemeState
+ {
+ Snapshot = snapshot,
+ Provider = provider,
+ };
+
+ // Atomic swap
+ Interlocked.Exchange(ref _currentState, newState);
+
+ _resourceSwapper.TryActivateTheme(provider.ThemeKey);
+ ThemeChanged?.Invoke(this, new ThemeChangedEventArgs());
+ }
+
+ private static BitmapImage? LoadImageSafe(string? path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return null;
+ }
+
+ try
+ {
+ // If it looks like a file path and exists, prefer absolute file URI
+ if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri))
+ {
+ return null;
+ }
+
+ if (!uri.IsAbsoluteUri && File.Exists(path))
+ {
+ uri = new Uri(Path.GetFullPath(path));
+ }
+
+ return new BitmapImage(uri);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning($"Failed to load background image '{path}'. {ex.Message}");
+ return null;
+ }
+ }
+
+ public ThemeService(SettingsModel settings, ResourceSwapper resourceSwapper)
+ {
+ ArgumentNullException.ThrowIfNull(settings);
+ ArgumentNullException.ThrowIfNull(resourceSwapper);
+
+ _settings = settings;
+ _settings.SettingsChanged += SettingsOnSettingsChanged;
+
+ _resourceSwapper = resourceSwapper;
+
+ _uiSettings = new UISettings();
+ _uiSettings.ColorValuesChanged += UiSettings_ColorValuesChanged;
+
+ _normalThemeProvider = new NormalThemeProvider(_uiSettings);
+ _colorfulThemeProvider = new ColorfulThemeProvider(_uiSettings);
+ List providers = [_normalThemeProvider, _colorfulThemeProvider];
+
+ foreach (var provider in providers)
+ {
+ _resourceSwapper.RegisterTheme(provider.ThemeKey, provider.ResourcePath);
+ }
+
+ _currentState = new InternalThemeState
+ {
+ Snapshot = new ThemeSnapshot
+ {
+ Tint = Colors.Transparent,
+ Theme = ElementTheme.Light,
+ BackdropParameters = new AcrylicBackdropParameters(Colors.Black, Colors.Black, 0.5f, 0.5f),
+ BackgroundImageOpacity = 1,
+ BackgroundImageSource = null,
+ BackgroundImageStretch = Stretch.Fill,
+ BlurAmount = 0,
+ TintIntensity = 1.0f,
+ BackgroundBrightness = 0,
+ },
+ Provider = _normalThemeProvider,
+ };
+ }
+
+ private void RequestReload()
+ {
+ if (!_isInitialized || _dispatcherQueueTimer is null)
+ {
+ return;
+ }
+
+ _dispatcherQueueTimer.Debounce(Reload, ReloadDebounceInterval);
+ }
+
+ private ElementTheme GetElementTheme(ElementTheme theme)
+ {
+ return theme switch
+ {
+ ElementTheme.Light => ElementTheme.Light,
+ ElementTheme.Dark => ElementTheme.Dark,
+ _ => _uiSettings.GetColorValue(UIColorType.Background).CalculateBrightness() < 0.5
+ ? ElementTheme.Dark
+ : ElementTheme.Light,
+ };
+ }
+
+ private void SettingsOnSettingsChanged(SettingsModel sender, object? args)
+ {
+ RequestReload();
+ }
+
+ private void UiSettings_ColorValuesChanged(UISettings sender, object args)
+ {
+ RequestReload();
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ _dispatcherQueueTimer?.Stop();
+ _uiSettings.ColorValuesChanged -= UiSettings_ColorValuesChanged;
+ _settings.SettingsChanged -= SettingsOnSettingsChanged;
+ }
+
+ private sealed class InternalThemeState
+ {
+ public required ThemeSnapshot Snapshot { get; init; }
+
+ public required IThemeProvider Provider { get; init; }
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/WindowThemeSynchronizer.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/WindowThemeSynchronizer.cs
new file mode 100644
index 0000000000..5c250b94ef
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/WindowThemeSynchronizer.cs
@@ -0,0 +1,70 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CmdPal.Core.Common.Services;
+using Microsoft.CmdPal.UI.ViewModels.Services;
+using Microsoft.UI.Xaml;
+
+namespace Microsoft.CmdPal.UI.Services;
+
+///
+/// Synchronizes a window's theme with .
+///
+internal sealed partial class WindowThemeSynchronizer : IDisposable
+{
+ private readonly IThemeService _themeService;
+ private readonly Window _window;
+
+ ///
+ /// Initializes a new instance of the class and subscribes to theme changes.
+ ///
+ /// The theme service to monitor for changes.
+ /// The window to synchronize.
+ /// Thrown when or is null.
+ public WindowThemeSynchronizer(IThemeService themeService, Window window)
+ {
+ _themeService = themeService ?? throw new ArgumentNullException(nameof(themeService));
+ _window = window ?? throw new ArgumentNullException(nameof(window));
+ _themeService.ThemeChanged += ThemeServiceOnThemeChanged;
+ }
+
+ ///
+ /// Unsubscribes from theme change events.
+ ///
+ public void Dispose()
+ {
+ _themeService.ThemeChanged -= ThemeServiceOnThemeChanged;
+ }
+
+ ///
+ /// Applies the current theme to the window when theme changes occur.
+ ///
+ private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
+ {
+ if (_window.Content is not FrameworkElement fe)
+ {
+ return;
+ }
+
+ var dispatcherQueue = fe.DispatcherQueue;
+
+ if (dispatcherQueue is not null && dispatcherQueue.HasThreadAccess)
+ {
+ ApplyRequestedTheme(fe);
+ }
+ else
+ {
+ dispatcherQueue?.TryEnqueue(() => ApplyRequestedTheme(fe));
+ }
+ }
+
+ private void ApplyRequestedTheme(FrameworkElement fe)
+ {
+ // LOAD BEARING: Changing the RequestedTheme to Dark then Light then target forces
+ // a refresh of the theme.
+ fe.RequestedTheme = ElementTheme.Dark;
+ fe.RequestedTheme = ElementTheme.Light;
+ fe.RequestedTheme = _themeService.Current.Theme;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/AppearancePage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/AppearancePage.xaml
new file mode 100644
index 0000000000..b9f31d8443
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/AppearancePage.xaml
@@ -0,0 +1,209 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/AppearancePage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/AppearancePage.xaml.cs
new file mode 100644
index 0000000000..39a8ea4ae1
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/AppearancePage.xaml.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+using ManagedCommon;
+using Microsoft.CmdPal.UI.ViewModels;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.UI;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Documents;
+using Microsoft.Windows.Storage.Pickers;
+
+namespace Microsoft.CmdPal.UI.Settings;
+
+///
+/// An empty page that can be used on its own or navigated to within a Frame.
+///
+public sealed partial class AppearancePage : Page
+{
+ private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
+
+ internal SettingsViewModel ViewModel { get; }
+
+ public AppearancePage()
+ {
+ InitializeComponent();
+
+ var settings = App.Current.Services.GetService()!;
+ ViewModel = new SettingsViewModel(settings, App.Current.Services, _mainTaskScheduler);
+ }
+
+ private async void PickBackgroundImage_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (XamlRoot?.ContentIslandEnvironment is null)
+ {
+ return;
+ }
+
+ var windowId = XamlRoot?.ContentIslandEnvironment?.AppWindowId ?? new WindowId(0);
+
+ var picker = new FileOpenPicker(windowId)
+ {
+ CommitButtonText = ViewModels.Properties.Resources.builtin_settings_appearance_pick_background_image_title!,
+ SuggestedStartLocation = PickerLocationId.PicturesLibrary,
+ ViewMode = PickerViewMode.Thumbnail,
+ };
+
+ string[] extensions = [".png", ".bmp", ".jpg", ".jpeg", ".jfif", ".gif", ".tiff", ".tif", ".webp", ".jxr"];
+ foreach (var ext in extensions)
+ {
+ picker.FileTypeFilter!.Add(ext);
+ }
+
+ var file = await picker.PickSingleFileAsync()!;
+ if (file != null)
+ {
+ ViewModel.Appearance.BackgroundImagePath = file.Path ?? string.Empty;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to pick background image file", ex);
+ }
+ }
+
+ private void OpenWindowsColorsSettings_Click(Hyperlink sender, HyperlinkClickEventArgs args)
+ {
+ // LOAD BEARING (or BEAR LOADING?): Process.Start with UseShellExecute inside a XAML input event can trigger WinUI reentrancy
+ // and cause FailFast crashes. Task.Run moves the call off the UI thread to prevent hard process termination.
+ Task.Run(() =>
+ {
+ try
+ {
+ _ = Process.Start(new ProcessStartInfo("ms-settings:colors") { UseShellExecute = true });
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to open Windows Settings", ex);
+ }
+ });
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml
index eb0264a683..65fa536c5b 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml
@@ -81,35 +81,10 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml
index dc3cf9fd3b..e451fe7abe 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml
@@ -62,6 +62,11 @@
x:Uid="Settings_GeneralPage_NavigationViewItem_General"
Icon="{ui:FontIcon Glyph=}"
Tag="General" />
+
typeof(GeneralPage),
+ "Appearance" => typeof(AppearancePage),
"Extensions" => typeof(ExtensionsPage),
_ => null,
};
-
if (pageType is not null)
{
NavFrame.Navigate(pageType);
@@ -248,6 +248,12 @@ public sealed partial class SettingsWindow : WindowEx,
var pageType = RS_.GetString("Settings_PageTitles_GeneralPage");
BreadCrumbs.Add(new(pageType, pageType));
}
+ else if (e.SourcePageType == typeof(AppearancePage))
+ {
+ NavView.SelectedItem = AppearancePageNavItem;
+ var pageType = RS_.GetString("Settings_PageTitles_AppearancePage");
+ BreadCrumbs.Add(new(pageType, pageType));
+ }
else if (e.SourcePageType == typeof(ExtensionsPage))
{
NavView.SelectedItem = ExtensionPageNavItem;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw
index 8fe053cae2..b2c0260a94 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw
@@ -550,9 +550,69 @@ Right-click to remove the key combination, thereby deactivating the shortcut.
Automatically returns to home page after a period of inactivity when Command Palette is closed
+
+ Personalization
+
+
+ App theme mode
+
+
+ Select which app theme to display
+
+
+ Appearance
+
+
+ Use system settings
+
+
+ Light
+
+
+ Dark
+
+
+ Color tint
+
+
+ Color intensity
+
+
+ Choose color
+
+
+ Use default
+
+
+ Use default color
+
+
+ Windows colors
+
+
+ Background image
+
+
+ Background image opacity
+
+
+ Background image fit
+
+
+ Fill
+
+
+ Fit
+
+
+ Stretch
+
General
+
+ Personalization
+
Extensions
@@ -577,4 +637,73 @@ Right-click to remove the key combination, thereby deactivating the shortcut.
Settings
+
+ Custom colors
+
+
+ None
+
+
+ Custom color
+
+
+ Accent color
+
+
+ Image
+
+
+ Browse...
+
+
+ Remove image
+
+
+ Background color
+
+
+ Choose a custom background color or use the current accent color
+
+
+ Background image
+
+
+ No settings
+
+
+ Background
+
+
+ Choose a custom background color or image
+
+
+ System accent color
+
+
+ Personalization › Colors
+
+
+ Background image blur
+
+
+ Background image brightness
+
+
+ Restore defaults
+
+
+ Reset
+
+
+ Change the system accent in Windows Settings:
+
+
+ Light
+
+
+ Dark
+
+
+ Use system settings
+
\ No newline at end of file
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Colors.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Colors.xaml
index edca3f479c..728cd3ef4e 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Colors.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Colors.xaml
@@ -4,37 +4,5 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Theme.Colorful.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Theme.Colorful.xaml
new file mode 100644
index 0000000000..e1dfe7f45c
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Theme.Colorful.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Theme.Normal.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Theme.Normal.xaml
new file mode 100644
index 0000000000..53b46d39d6
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Theme.Normal.xaml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+