CmdPal: Light, dark, pink, and unicorns (#43505)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR introduces user settings for app mode themes (dark, light, or
system) and background customization options, including custom colors,
system accent colors, or custom images.

- Adds a new page to the Settings window with new appearance settings
and moves some existing settings there as well.
- Introduces a new core-level service abstraction, `IThemeService`, that
holds the state for the current theme.
- Uses the helper class `ResourceSwapper` to update application-level
XAML resources. The way WinUI / XAML handles these is painful, and XAML
Hot Reload is pain². Initialization must be lazy, as XAML resources can
only be accessed after the window is activated.
- `ThemeService` takes app and system settings and selects one of the
registered `IThemeProvider`s to calculate visuals and choose the
appropriate XAML resources.
  - At the moment, there are two:
    - `NormalThemeProvider`
      - Provides the current uncolorized light and dark styles  
      - `ms-appx:///Styles/Theme.Normal.xaml`
    - `ColorfulThemeProvider`
- Style that matches the Windows 11 visual style (based on the Start
menu) and colors
      - `ms-appx:///Styles/Theme.Colorful.xaml`  
- Applied when the background is colorized or a background image is
selected
- The app theme is applied only on the main window
(`WindowThemeSynchronizer` helper class can be used to synchronize other
windows if needed).
- Adds a new dependency on `Microsoft.Graphics.Win2D`.
- Adds a custom color picker popup; the one from the Community Toolkit
occasionally loses the selected color.
- Flyby: separates the keyword tag and localizable label for pages in
the Settings window navigation.


## Pictures? Pictures!

<img width="2027" height="1276" alt="image"
src="https://github.com/user-attachments/assets/e3485c71-7faa-495b-b455-b313ea6046ee"
/>

<img width="3776" height="2025" alt="image"
src="https://github.com/user-attachments/assets/820fa823-34d4-426d-b066-b1049dc3266f"
/>

Matching Windows accent color and tint:

<img width="3840" height="2160" alt="image"
src="https://github.com/user-attachments/assets/65f3b608-e282-4894-b7c8-e014a194f11f"
/>


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #38444
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
This commit is contained in:
Jiří Polášek
2025-12-10 01:56:03 +01:00
committed by GitHub
parent 0f8cf94d90
commit 97c1de8bf6
58 changed files with 4141 additions and 115 deletions

View File

@@ -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}";
}

View File

@@ -0,0 +1,216 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.ColorPalette"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
xmlns:localConverters="using:Microsoft.CmdPal.UI.Converters"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:toolkitConverters="using:CommunityToolkit.WinUI.Converters"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<UserControl.Resources>
<toolkitConverters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<localConverters:ContrastBrushConverter x:Key="ContrastBrushConverter" />
<Style x:Key="PaletteGridViewItemStyle" TargetType="GridViewItem">
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{ThemeResource SystemControlForegroundBaseHighBrush}" />
<Setter Property="TabNavigation" Value="Local" />
<Setter Property="IsHoldingEnabled" Value="True" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="MinWidth" Value="32" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="AllowDrop" Value="False" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-2" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewItem">
<Grid
x:Name="ContentBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Control.IsTemplateFocusTarget="True"
CornerRadius="{TemplateBinding CornerRadius}"
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform>
<ScaleTransform x:Name="ContentBorderScale" />
</Grid.RenderTransform>
<ContentPresenter
x:Name="ContentPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
ContentTransitions="{TemplateBinding ContentTransitions}" />
<!--
The 'Xg' text simulates the amount of space one line of text will occupy.
In the DataPlaceholder state, the Content is not loaded yet so we
approximate the size of the item using placeholder text.
-->
<TextBlock
x:Name="PlaceholderTextBlock"
Margin="{TemplateBinding Padding}"
AutomationProperties.AccessibilityView="Raw"
Foreground="{x:Null}"
IsHitTestVisible="False"
Text="Xg"
Visibility="Collapsed" />
<Rectangle
x:Name="PlaceholderRect"
Fill="{ThemeResource ListViewItemPlaceholderBackground}"
Visibility="Collapsed" />
<Rectangle
x:Name="BorderRectangle"
IsHitTestVisible="False"
Opacity="0"
RadiusX="6"
RadiusY="6"
Stroke="{ThemeResource SystemControlHighlightListAccentLowBrush}"
StrokeThickness="2" />
<Border
x:Name="MultiSelectSquare"
Width="20"
Height="20"
Margin="0,2,2,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="{ThemeResource SystemControlBackgroundChromeMediumBrush}"
CornerRadius="6"
Visibility="Collapsed">
<FontIcon
x:Name="MultiSelectCheck"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="16"
Foreground="{ThemeResource SystemControlForegroundBaseMediumHighBrush}"
Glyph="&#xE73E;"
Opacity="0" />
</Border>
<Border
x:Name="MultiArrangeOverlayTextBorder"
Height="20"
MinWidth="20"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{ThemeResource SystemControlBackgroundAccentBrush}"
BorderBrush="{ThemeResource SystemControlBackgroundChromeWhiteBrush}"
BorderThickness="2"
CornerRadius="6"
IsHitTestVisible="False"
Opacity="0">
<TextBlock
x:Name="MultiArrangeOverlayText"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
IsHitTestVisible="False"
Opacity="0"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.DragItemsCount}" />
</Border>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="FocusStates">
<VisualState x:Name="Focused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderRectangle" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Unfocused" />
</VisualStateGroup>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<Storyboard>
<PointerUpThemeAnimation Storyboard.TargetName="ContentPresenter" />
</Storyboard>
</VisualState>
<VisualState x:Name="PointerOver">
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="BorderRectangle"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderRectangle" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentBorder" Storyboard.TargetProperty="FocusVisualSecondaryBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentBorder" Storyboard.TargetProperty="FocusVisualSecondaryThickness">
<DiscreteObjectKeyFrame KeyTime="0" Value="2" />
</ObjectAnimationUsingKeyFrames>
<PointerUpThemeAnimation Storyboard.TargetName="ContentPresenter" />
<DoubleAnimation
Storyboard.TargetName="MultiSelectCheck"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="MultiSelectSquare" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={ThemeResource TextControlForeground}}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid HorizontalAlignment="Stretch">
<GridView
Margin="0"
Padding="0"
IsItemClickEnabled="True"
ItemClick="ListViewBase_OnItemClick"
ItemContainerStyle="{StaticResource PaletteGridViewItemStyle}"
ItemsSource="{x:Bind PaletteColors}"
SelectionMode="None">
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<controls:UniformGrid ui:FrameworkElementExtensions.AncestorType="local:ColorPalette" Columns="{Binding (ui:FrameworkElementExtensions.Ancestor).CustomPaletteColumnCount, RelativeSource={RelativeSource Self}}" />
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.ItemTemplate>
<DataTemplate x:DataType="Color">
<Border
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AutomationProperties.Name="{Binding Converter={StaticResource ColorToDisplayNameConverter}}"
CornerRadius="4"
ToolTipService.ToolTip="{Binding Converter={StaticResource ColorToDisplayNameConverter}}">
<Border.Background>
<SolidColorBrush Color="{Binding}" />
</Border.Background>
</Border>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</Grid>
</UserControl>

View File

@@ -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<Color>), 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<Color?>? 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<Color> PaletteColors
{
get => (ObservableCollection<Color>)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);
}
}
}

View File

@@ -0,0 +1,90 @@
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.ColorPickerButton"
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:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="300"
d:DesignWidth="400"
mc:Ignorable="d">
<UserControl.Resources>
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolToVisibilityConverter
x:Key="BoolToVisibilityInvertedConverter"
FalseValue="Visible"
TrueValue="Collapsed" />
</UserControl.Resources>
<StackPanel Orientation="Horizontal" Spacing="8">
<DropDownButton Padding="{x:Bind ToDropDownPadding(HasSelectedColor), Mode=OneWay}">
<Grid>
<TextBlock x:Uid="OptionalColorPickerButton_UnsetTextBlock" Visibility="{x:Bind HasSelectedColor, Mode=OneWay, Converter={StaticResource BoolToVisibilityInvertedConverter}}" />
<Border
x:Name="ColorPreviewBorder"
Width="48"
Height="24"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{ThemeResource ControlCornerRadius}"
Visibility="{x:Bind HasSelectedColor, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<Border.Background>
<SolidColorBrush Color="{x:Bind SelectedColor, Mode=OneWay}" />
</Border.Background>
</Border>
</Grid>
<DropDownButton.Flyout>
<Flyout
x:Name="ColorPickerFlyout"
Opened="FlyoutBase_OnOpened"
Placement="Bottom"
ShouldConstrainToRootBounds="False">
<Flyout.FlyoutPresenterStyle>
<Style BasedOn="{StaticResource DefaultFlyoutPresenterStyle}" TargetType="FlyoutPresenter">
<Setter Property="MinWidth" Value="660" />
</Style>
</Flyout.FlyoutPresenterStyle>
<StackPanel
x:Name="FlyoutRoot"
Orientation="Horizontal"
SizeChanged="FlyoutRoot_OnSizeChanged"
Spacing="20">
<!-- Left column: Preset colors and reset button -->
<StackPanel Margin="2">
<TextBlock
x:Uid="OptionalColorPickerButton_WindowsColorsSectionHeading"
Margin="0,0,0,12"
Style="{StaticResource BodyTextBlockStyle}" />
<controls:ColorPalette
HorizontalAlignment="Left"
VerticalAlignment="Top"
CustomPaletteColumnCount="9"
PaletteColors="{x:Bind PaletteColors}"
SelectedColorChanged="ColorPalette_OnSelectedColorChanged" />
</StackPanel>
<!-- Right column: Spectrum -->
<StackPanel>
<TextBlock
x:Uid="OptionalColorPickerButton_CustomColorsSectionHeading"
Margin="0,0,0,12"
Style="{StaticResource BodyTextBlockStyle}" />
<ColorPicker
IsAlphaEnabled="{x:Bind IsAlphaEnabled, Mode=OneWay}"
IsAlphaSliderVisible="{x:Bind IsAlphaEnabled, Mode=OneWay}"
IsAlphaTextInputVisible="{x:Bind IsAlphaEnabled, Mode=OneWay}"
IsColorChannelTextInputVisible="True"
IsColorSliderVisible="True"
IsHexInputVisible="True"
IsMoreButtonVisible="True"
Color="{x:Bind SelectedColor, Mode=TwoWay}" />
</StackPanel>
</StackPanel>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
</StackPanel>
</UserControl>

View File

@@ -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<Color>), typeof(ColorPickerButton), new PropertyMetadata(new ObservableCollection<Color>()))!;
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<Color> PaletteColors
{
get { return (ObservableCollection<Color>)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();
}
}

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.CommandPalettePreview"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:h="using:Microsoft.CmdPal.UI.Helpers"
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Border
Width="200"
Height="120"
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Translation="0,0,8">
<Grid>
<Border
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="1"
Visibility="{x:Bind h:BindTransformers.NegateVisibility(ShowBackgroundImage), Mode=OneWay}">
<Border.Background>
<AcrylicBrush
FallbackColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
TintColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
TintOpacity="{x:Bind PreviewBackgroundOpacity, Mode=OneWay}" />
</Border.Background>
</Border>
<local:BlurImageControl
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BlurAmount="{x:Bind PreviewBackgroundImageBlurAmount, Mode=OneWay}"
ImageBrightness="{x:Bind PreviewBackgroundImageBrightness, Mode=OneWay}"
ImageSource="{x:Bind PreviewBackgroundImageSource, Mode=OneWay}"
ImageStretch="{x:Bind ToStretch(PreviewBackgroundImageFit), Mode=OneWay}"
IsHitTestVisible="False"
TintColor="{x:Bind PreviewBackgroundImageTint, Mode=OneWay}"
TintIntensity="{x:Bind ToTintIntensity(PreviewBackgroundImageTintIntensity), Mode=OneWay}"
Visibility="{x:Bind ShowBackgroundImage, Mode=OneWay}" />
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<Border
x:Name="ContentPreview"
Grid.Row="0"
Background="{ThemeResource LayerOnAcrylicPrimaryBackgroundBrush}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border BorderBrush="{ThemeResource CmdPal.TopBarBorderBrush}" BorderThickness="0,0,0,1" />
</Grid>
</Border>
<Border
x:Name="CommandBarPreview"
Grid.Row="1"
Background="{ThemeResource LayerOnAcrylicSecondaryBackgroundBrush}"
BorderBrush="{ThemeResource CmdPal.CommandBarBorderBrush}"
BorderThickness="0,1,0,0" />
</Grid>
</Grid>
</Border>
</UserControl>

View File

@@ -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,
};
}
}

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.ScreenPreview"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Border
x:Name="ScreenBorder"
HorizontalAlignment="Center"
VerticalAlignment="Center"
BorderBrush="Black"
BorderThickness="8"
CornerRadius="8"
UseLayoutRounding="True">
<Viewbox Height="120" UseLayoutRounding="True">
<Grid>
<Image
x:Name="WallpaperImage"
MaxHeight="200"
Stretch="Uniform"
UseLayoutRounding="True" />
<ContentPresenter
x:Name="ContentPresenter"
Margin="40"
Content="{x:Bind PreviewContent}"
UseLayoutRounding="True" />
</Grid>
</Viewbox>
</Border>
</UserControl>

View File

@@ -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());
}
}

View File

@@ -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}"