diff --git a/Directory.Build.props b/Directory.Build.props
index d0e8f102..51c3f6dd 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -10,6 +10,7 @@
$(RepositoryDirectory)components\Converters\src\CommunityToolkit.WinUI.Converters.csproj$(RepositoryDirectory)components\Extensions\src\CommunityToolkit.WinUI.Extensions.csproj$(RepositoryDirectory)components\Triggers\src\CommunityToolkit.WinUI.Triggers.csproj
+ $(RepositoryDirectory)components\Segmented\src\CommunityToolkit.WinUI.Controls.Segmented.csproj$(RepositoryDirectory)components\SettingsControls\src\CommunityToolkit.WinUI.Controls.SettingsControls.csproj
diff --git a/components/ColorPicker/OpenSolution.bat b/components/ColorPicker/OpenSolution.bat
new file mode 100644
index 00000000..814a56d4
--- /dev/null
+++ b/components/ColorPicker/OpenSolution.bat
@@ -0,0 +1,3 @@
+@ECHO OFF
+
+powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %*
\ No newline at end of file
diff --git a/components/ColorPicker/samples/Assets/ColorPicker.png b/components/ColorPicker/samples/Assets/ColorPicker.png
new file mode 100644
index 00000000..3fbba28b
Binary files /dev/null and b/components/ColorPicker/samples/Assets/ColorPicker.png differ
diff --git a/components/ColorPicker/samples/ColorPicker.Samples.csproj b/components/ColorPicker/samples/ColorPicker.Samples.csproj
new file mode 100644
index 00000000..741a87ef
--- /dev/null
+++ b/components/ColorPicker/samples/ColorPicker.Samples.csproj
@@ -0,0 +1,18 @@
+
+
+
+ ColorPicker
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
diff --git a/components/ColorPicker/samples/ColorPicker.md b/components/ColorPicker/samples/ColorPicker.md
new file mode 100644
index 00000000..94696882
--- /dev/null
+++ b/components/ColorPicker/samples/ColorPicker.md
@@ -0,0 +1,22 @@
+---
+title: ColorPicker
+author: michael-hawker
+description: An extended color picker control that lets a user pick a color using a color spectrum, sliders, or text input.
+keywords: ColorPicker, Control, Layout
+dev_langs:
+ - csharp
+category: Controls
+subcategory: Input
+discussion-id: 0
+issue-id: 0
+icon: Assets/ColorPicker.png
+---
+
+The [ColorPicker](/dotnet/api/microsoft.toolkit.uwp.ui.controls.colorpicker) control lets a user pick a color using a color spectrum, palette, sliders, or text input.
+
+> [!Sample ColorPickerSample]
+
+## ColorPickerButton
+The `ColorPickerButton` variant represents a `DropDownButton` variant which provides a preview of the selected color and allows a user to expand the drop-down to select a new color.
+
+> [!Sample ColorPickerButtonSample]
diff --git a/components/ColorPicker/samples/ColorPickerButtonSample.xaml b/components/ColorPicker/samples/ColorPickerButtonSample.xaml
new file mode 100644
index 00000000..c473c4b2
--- /dev/null
+++ b/components/ColorPicker/samples/ColorPickerButtonSample.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/components/ColorPicker/samples/ColorPickerButtonSample.xaml.cs b/components/ColorPicker/samples/ColorPickerButtonSample.xaml.cs
new file mode 100644
index 00000000..f2e1bb19
--- /dev/null
+++ b/components/ColorPicker/samples/ColorPickerButtonSample.xaml.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using ColorSpectrumShape = Microsoft.UI.Xaml.Controls.ColorSpectrumShape;
+
+namespace ColorPickerExperiment.Samples;
+
+[ToolkitSample(id: nameof(ColorPickerButtonSample), "ColorPickerButton", description: $"A sample for showing how to create and use a {nameof(CommunityToolkit.WinUI.Controls.ColorPickerButton)} control.")]
+public sealed partial class ColorPickerButtonSample : Page
+{
+ public ColorPickerButtonSample()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/ColorPicker/samples/ColorPickerSample.xaml b/components/ColorPicker/samples/ColorPickerSample.xaml
new file mode 100644
index 00000000..9ad7a693
--- /dev/null
+++ b/components/ColorPicker/samples/ColorPickerSample.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
diff --git a/components/ColorPicker/samples/ColorPickerSample.xaml.cs b/components/ColorPicker/samples/ColorPickerSample.xaml.cs
new file mode 100644
index 00000000..f632aa9c
--- /dev/null
+++ b/components/ColorPicker/samples/ColorPickerSample.xaml.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using ColorSpectrumShape = Microsoft.UI.Xaml.Controls.ColorSpectrumShape;
+
+namespace ColorPickerExperiment.Samples;
+
+[ToolkitSampleBoolOption("AccentColors", true, Title = "ShowAccentColors")]
+[ToolkitSampleBoolOption("AlphaEnabled", true, Title = "IsAlphaEnabled")]
+[ToolkitSampleBoolOption("AlphaSlider", true, Title = "IsAlphaSliderVisible")]
+[ToolkitSampleBoolOption("ColorSlider", true, Title = "IsColorSliderVisible")]
+[ToolkitSampleBoolOption("ColorChannel", true, Title = "IsColorChannelTextInputVisible")]
+[ToolkitSampleBoolOption("SpectrumVisible", true, Title = "IsColorSpectrumVisible")]
+[ToolkitSampleBoolOption("ColorPalette", true, Title = "IsColorPaletteVisible")]
+
+[ToolkitSampleMultiChoiceOption("SpectrumShape", "Box", "Ring", Title = "ColorSpectrumShape")]
+
+[ToolkitSample(id: nameof(ColorPickerSample), "ColorPicker", description: $"A sample for showing how to create and use a {nameof(CommunityToolkit.WinUI.Controls.ColorPicker)} control.")]
+public sealed partial class ColorPickerSample : Page
+{
+ public ColorPickerSample()
+ {
+ this.InitializeComponent();
+ }
+
+ // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149
+ public static ColorSpectrumShape ConvertStringToColorSpectrumShape(string shape) => shape switch
+ {
+ "Ring" => ColorSpectrumShape.Ring,
+ "Box" => ColorSpectrumShape.Box,
+ _ => throw new System.NotImplementedException(),
+ };
+}
diff --git a/components/ColorPicker/samples/Dependencies.props b/components/ColorPicker/samples/Dependencies.props
new file mode 100644
index 00000000..e622e1df
--- /dev/null
+++ b/components/ColorPicker/samples/Dependencies.props
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/ColorPicker/src/ColorChannel.cs b/components/ColorPicker/src/ColorChannel.cs
new file mode 100644
index 00000000..25a7e6c5
--- /dev/null
+++ b/components/ColorPicker/src/ColorChannel.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// Defines a specific channel within a color representation.
+///
+[EditorBrowsable(EditorBrowsableState.Advanced)]
+public enum ColorChannel
+{
+ ///
+ /// Represents the alpha channel.
+ ///
+ Alpha,
+
+ ///
+ /// Represents the first color channel which is Red when RGB or Hue when HSV.
+ ///
+ Channel1,
+
+ ///
+ /// Represents the second color channel which is Green when RGB or Saturation when HSV.
+ ///
+ Channel2,
+
+ ///
+ /// Represents the third color channel which is Blue when RGB or Value when HSV.
+ ///
+ Channel3
+}
diff --git a/components/ColorPicker/src/ColorPicker.Properties.cs b/components/ColorPicker/src/ColorPicker.Properties.cs
new file mode 100644
index 00000000..509aef54
--- /dev/null
+++ b/components/ColorPicker/src/ColorPicker.Properties.cs
@@ -0,0 +1,143 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// Contains all properties for the .
+///
+public partial class ColorPicker
+{
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty CustomPaletteColorsProperty =
+ DependencyProperty.Register(
+ nameof(CustomPaletteColors),
+ typeof(ObservableCollection),
+ typeof(ColorPicker),
+ new PropertyMetadata(
+ null,
+ (s, e) => (s as ColorPicker)?.OnDependencyPropertyChanged(s, e)));
+
+ ///
+ /// Gets the list of custom palette colors.
+ ///
+ public ObservableCollection CustomPaletteColors
+ {
+ get => (ObservableCollection)this.GetValue(CustomPaletteColorsProperty);
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty CustomPaletteColumnCountProperty =
+ DependencyProperty.Register(
+ nameof(CustomPaletteColumnCount),
+ typeof(int),
+ typeof(ColorPicker),
+ new PropertyMetadata(
+ 4,
+ (s, e) => (s as ColorPicker)?.OnDependencyPropertyChanged(s, e)));
+
+ ///
+ /// Gets or sets the number of colors in each row (section) of the custom color palette.
+ /// Within a standard palette, rows are shades and columns are unique colors.
+ ///
+ public int CustomPaletteColumnCount
+ {
+ get => (int)this.GetValue(CustomPaletteColumnCountProperty);
+ set
+ {
+ if (object.Equals(value, this.GetValue(CustomPaletteColumnCountProperty)) == false)
+ {
+ this.SetValue(CustomPaletteColumnCountProperty, value);
+ }
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty CustomPaletteProperty =
+ DependencyProperty.Register(
+ nameof(CustomPalette),
+ typeof(IColorPalette),
+ typeof(ColorPicker),
+ new PropertyMetadata(
+ null,
+ (s, e) => (s as ColorPicker)?.OnDependencyPropertyChanged(s, e)));
+
+ ///
+ /// Gets or sets the custom color palette.
+ /// This will automatically set and
+ /// overwriting any existing values.
+ ///
+ public IColorPalette CustomPalette
+ {
+ get => (IColorPalette)this.GetValue(CustomPaletteProperty);
+ set
+ {
+ if (object.Equals(value, this.GetValue(CustomPaletteProperty)) == false)
+ {
+ this.SetValue(CustomPaletteProperty, value);
+ }
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty IsColorPaletteVisibleProperty =
+ DependencyProperty.Register(
+ nameof(IsColorPaletteVisible),
+ typeof(bool),
+ typeof(ColorPicker),
+ new PropertyMetadata(
+ true,
+ (s, e) => (s as ColorPicker)?.OnDependencyPropertyChanged(s, e)));
+
+ ///
+ /// Gets or sets a value indicating whether the color palette is visible.
+ ///
+ public bool IsColorPaletteVisible
+ {
+ get => (bool)this.GetValue(IsColorPaletteVisibleProperty);
+ set
+ {
+ if (object.Equals(value, this.GetValue(IsColorPaletteVisibleProperty)) == false)
+ {
+ this.SetValue(IsColorPaletteVisibleProperty, value);
+ }
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ShowAccentColorsProperty =
+ DependencyProperty.Register(
+ nameof(ShowAccentColors),
+ typeof(bool),
+ typeof(ColorPicker),
+ new PropertyMetadata(
+ true,
+ (s, e) => (s as ColorPicker)?.OnDependencyPropertyChanged(s, e)));
+
+ ///
+ /// Gets or sets a value indicating whether accent colors are shown along
+ /// with the preview color.
+ ///
+ public bool ShowAccentColors
+ {
+ get => (bool)this.GetValue(ShowAccentColorsProperty);
+ set
+ {
+ if (object.Equals(value, this.GetValue(ShowAccentColorsProperty)) == false)
+ {
+ this.SetValue(ShowAccentColorsProperty, value);
+ }
+ }
+ }
+}
diff --git a/components/ColorPicker/src/ColorPicker.cs b/components/ColorPicker/src/ColorPicker.cs
new file mode 100644
index 00000000..eb5e0690
--- /dev/null
+++ b/components/ColorPicker/src/ColorPicker.cs
@@ -0,0 +1,1451 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI.Controls.Primitives;
+using CommunityToolkit.WinUI.Helpers;
+using System.Collections.Specialized;
+using Windows.UI;
+using ColorSpectrum = Microsoft.UI.Xaml.Controls.Primitives.ColorSpectrum;
+using ColorPickerSlider = CommunityToolkit.WinUI.Controls.Primitives.ColorPickerSlider;
+#if WINAPPSDK || HAS_UNO && WINUI
+using Microsoft.UI.Xaml.Input;
+using Microsoft.UI.Dispatching;
+using Colors = Microsoft.UI.Colors;
+#elif WINDOWS_UWP || HAS_UNO && WINUI2
+using Windows.System;
+using Windows.UI.Xaml.Input;
+using Microsoft.UI.Xaml.Controls;
+using Colors = Windows.UI.Colors;
+#endif
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// Presents a color spectrum, a palette of colors, and color channel sliders for user selection of a color.
+///
+[TemplatePart(Name = nameof(ColorPicker.AlphaChannelNumberBox), Type = typeof(NumberBox))]
+[TemplatePart(Name = nameof(ColorPicker.AlphaChannelSlider), Type = typeof(ColorPickerSlider))]
+[TemplatePart(Name = nameof(ColorPicker.Channel1NumberBox), Type = typeof(NumberBox))]
+[TemplatePart(Name = nameof(ColorPicker.Channel1Slider), Type = typeof(ColorPickerSlider))]
+[TemplatePart(Name = nameof(ColorPicker.Channel2NumberBox), Type = typeof(NumberBox))]
+[TemplatePart(Name = nameof(ColorPicker.Channel2Slider), Type = typeof(ColorPickerSlider))]
+[TemplatePart(Name = nameof(ColorPicker.Channel3NumberBox), Type = typeof(NumberBox))]
+[TemplatePart(Name = nameof(ColorPicker.Channel3Slider), Type = typeof(ColorPickerSlider))]
+[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground1Border), Type = typeof(Border))]
+[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground2Border), Type = typeof(Border))]
+[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground3Border), Type = typeof(Border))]
+[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground4Border), Type = typeof(Border))]
+[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground5Border), Type = typeof(Border))]
+[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground6Border), Type = typeof(Border))]
+[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground7Border), Type = typeof(Border))]
+[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground8Border), Type = typeof(Border))]
+[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground9Border), Type = typeof(Border))]
+[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground10Border), Type = typeof(Border))]
+[TemplatePart(Name = nameof(ColorPicker.ColorPanelSelector), Type = typeof(Segmented))]
+[TemplatePart(Name = nameof(ColorPicker.ColorSpectrumControl), Type = typeof(ColorSpectrum))]
+[TemplatePart(Name = nameof(ColorPicker.ColorSpectrumAlphaSlider), Type = typeof(ColorPickerSlider))]
+[TemplatePart(Name = nameof(ColorPicker.ColorSpectrumThirdDimensionSlider), Type = typeof(ColorPickerSlider))]
+[TemplatePart(Name = nameof(ColorPicker.HexInputTextBox), Type = typeof(TextBox))]
+[TemplatePart(Name = nameof(ColorPicker.ColorModeComboBox), Type = typeof(ComboBox))]
+
+[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1501:Statement should not be on a single line", Justification = "Inline brackets are used to improve code readability with repeated null checks.")]
+[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "Whitespace is used to align code in columns for readability.")]
+[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Only template parts start with a capital letter. This differentiates them from other fields.")]
+public partial class ColorPicker : Microsoft.UI.Xaml.Controls.ColorPicker
+{
+ internal Color CheckerBackgroundColor { get; set; } = Color.FromArgb(0x19, 0x80, 0x80, 0x80); // Overridden later
+
+ ///
+ /// The period that scheduled color updates will be applied.
+ /// This is only used when updating colors using the ScheduleColorUpdate() method.
+ /// Color changes made directly to the Color property will apply instantly.
+ ///
+ private const int ColorUpdateInterval = 30; // Milliseconds
+
+ private long tokenColor;
+
+ private bool callbacksConnected = false;
+ private bool eventsConnected = false;
+ private bool isInitialized = false;
+
+ // Color information for updates
+ private HsvColor? savedHsvColor = null;
+ private Color? savedHsvColorRgbEquivalent = null;
+ private Color? updatedRgbColor = null;
+ private DispatcherQueueTimer? dispatcherQueueTimer = null;
+
+ private Segmented ColorPanelSelector;
+ private ColorSpectrum ColorSpectrumControl;
+ private ColorPickerSlider ColorSpectrumAlphaSlider;
+ private ColorPickerSlider ColorSpectrumThirdDimensionSlider;
+ private TextBox HexInputTextBox;
+ private ComboBox ColorModeComboBox;
+
+ private NumberBox Channel1NumberBox;
+ private NumberBox Channel2NumberBox;
+ private NumberBox Channel3NumberBox;
+ private NumberBox AlphaChannelNumberBox;
+ private ColorPickerSlider Channel1Slider;
+ private ColorPickerSlider Channel2Slider;
+ private ColorPickerSlider Channel3Slider;
+ private ColorPickerSlider AlphaChannelSlider;
+
+ private ColorPreviewer ColorPreviewer;
+
+ // Up to 10 checkered backgrounds may be used by name anywhere in the template
+ private Border CheckeredBackground1Border;
+ private Border CheckeredBackground2Border;
+ private Border CheckeredBackground3Border;
+ private Border CheckeredBackground4Border;
+ private Border CheckeredBackground5Border;
+ private Border CheckeredBackground6Border;
+ private Border CheckeredBackground7Border;
+ private Border CheckeredBackground8Border;
+ private Border CheckeredBackground9Border;
+ private Border CheckeredBackground10Border;
+
+ /***************************************************************************************
+ *
+ * Constructor/Destructor
+ *
+ ***************************************************************************************/
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
+ public ColorPicker()
+#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
+ {
+ this.DefaultStyleKey = typeof(ColorPicker);
+
+ // Workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/3502
+ this.DefaultStyleResourceUri = new Uri("ms-appx:///CommunityToolkit.WinUI.Controls.ColorPicker/Themes/Generic.xaml");
+
+ // Setup collections
+ this.SetValue(CustomPaletteColorsProperty, new ObservableCollection());
+ this.CustomPaletteColors.CollectionChanged += CustomPaletteColors_CollectionChanged;
+
+ this.Loaded += ColorPickerButton_Loaded;
+
+ // Checkered background color is found only one time for performance
+ // This may need to change in the future if theme changes should be supported
+ this.CheckerBackgroundColor = (Color)Application.Current.Resources["SystemListLowColor"];
+
+ this.ConnectCallbacks(true);
+ this.SetDefaultPalette();
+ this.StartDispatcherQueueTimer();
+ this.RegisterPropertyChangedCallback(IsColorChannelTextInputVisibleProperty, OnPanelVisibilityChanged);
+ this.RegisterPropertyChangedCallback(IsColorSpectrumVisibleProperty, OnPanelVisibilityChanged);
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~ColorPicker()
+ {
+ this.StopDispatcherQueueTimer();
+ this.CustomPaletteColors.CollectionChanged -= CustomPaletteColors_CollectionChanged;
+ }
+
+ /***************************************************************************************
+ *
+ * Methods
+ *
+ ***************************************************************************************/
+
+ ///
+ /// Gets whether or not the color is considered empty (all fields zero).
+ /// In the future Color.IsEmpty will hopefully be added to UWP.
+ ///
+ /// The Windows.UI.Color to calculate with.
+ /// Whether the color is considered empty.
+ private static bool IsColorEmpty(Color color)
+ {
+ return color.A == 0x00 &&
+ color.R == 0x00 &&
+ color.G == 0x00 &&
+ color.B == 0x00;
+ }
+
+ ///
+ /// Overrides when a template is applied in order to get the required controls.
+ ///
+ protected override void OnApplyTemplate()
+ {
+ // We need to disconnect old events first
+ this.ConnectEvents(false);
+
+ this.ColorPanelSelector = (Segmented)GetTemplateChild(nameof(ColorPanelSelector));
+
+ this.ColorSpectrumControl = (ColorSpectrum)GetTemplateChild(nameof(ColorSpectrumControl));
+ this.ColorSpectrumAlphaSlider = (ColorPickerSlider)this.GetTemplateChild(nameof(ColorSpectrumAlphaSlider));
+ this.ColorSpectrumThirdDimensionSlider = (ColorPickerSlider)this.GetTemplateChild(nameof(ColorSpectrumThirdDimensionSlider));
+
+ this.HexInputTextBox = (TextBox)this.GetTemplateChild(nameof(HexInputTextBox));
+ this.ColorModeComboBox = (ComboBox)this.GetTemplateChild(nameof(ColorModeComboBox));
+
+ this.Channel1NumberBox = (NumberBox)this.GetTemplateChild(nameof(Channel1NumberBox));
+ this.Channel2NumberBox = (NumberBox)this.GetTemplateChild(nameof(Channel2NumberBox));
+ this.Channel3NumberBox = (NumberBox)this.GetTemplateChild(nameof(Channel3NumberBox));
+ this.AlphaChannelNumberBox = (NumberBox)this.GetTemplateChild(nameof(AlphaChannelNumberBox));
+
+ this.Channel1Slider = (ColorPickerSlider)this.GetTemplateChild(nameof(Channel1Slider));
+ this.Channel2Slider = (ColorPickerSlider)this.GetTemplateChild(nameof(Channel2Slider));
+ this.Channel3Slider = (ColorPickerSlider)this.GetTemplateChild(nameof(Channel3Slider));
+ this.AlphaChannelSlider = (ColorPickerSlider)this.GetTemplateChild(nameof(AlphaChannelSlider));
+
+ this.ColorPreviewer = (ColorPreviewer)this.GetTemplateChild(nameof(ColorPreviewer));
+
+ this.CheckeredBackground1Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground1Border));
+ this.CheckeredBackground2Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground2Border));
+ this.CheckeredBackground3Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground3Border));
+ this.CheckeredBackground4Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground4Border));
+ this.CheckeredBackground5Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground5Border));
+ this.CheckeredBackground6Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground6Border));
+ this.CheckeredBackground7Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground7Border));
+ this.CheckeredBackground8Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground8Border));
+ this.CheckeredBackground9Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground9Border));
+ this.CheckeredBackground10Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground10Border));
+
+ // Must connect after controls are resolved
+ this.ConnectEvents(true);
+
+ base.OnApplyTemplate();
+ this.UpdateVisualState(false);
+ this.ValidateSelectedPanel();
+ this.isInitialized = true;
+
+ this.SetActiveColorRepresentation(ColorRepresentation.Rgba);
+ this.UpdateColorControlValues(); // TODO: This will also connect events after, can we optimize vs. doing it twice with the ConnectEvents above?
+ }
+
+ ///
+ /// Connects or disconnects all dependency property callbacks.
+ ///
+ private void ConnectCallbacks(bool connected)
+ {
+ if (connected == true &&
+ this.callbacksConnected == false)
+ {
+ // Add callbacks for dependency properties
+ this.tokenColor = this.RegisterPropertyChangedCallback(ColorProperty, OnColorChanged);
+
+ this.callbacksConnected = true;
+ }
+ else if (connected == false &&
+ this.callbacksConnected == true)
+ {
+ // Remove callbacks for dependency properties
+ this.UnregisterPropertyChangedCallback(ColorProperty, this.tokenColor);
+
+ this.callbacksConnected = false;
+ }
+
+ return;
+ }
+
+ ///
+ /// Connects or disconnects all control event handlers.
+ ///
+ /// True to connect event handlers, otherwise false.
+ private void ConnectEvents(bool connected)
+ {
+ if (connected == true &&
+ this.eventsConnected == false)
+ {
+ // Add all events
+ if (this.ColorSpectrumControl != null) { this.ColorSpectrumControl.ColorChanged += ColorSpectrum_ColorChanged; }
+ if (this.ColorSpectrumControl != null) { this.ColorSpectrumControl.GotFocus += ColorSpectrum_GotFocus; }
+ if (this.HexInputTextBox != null) { this.HexInputTextBox.KeyDown += HexInputTextBox_KeyDown; }
+ if (this.HexInputTextBox != null) { this.HexInputTextBox.LostFocus += HexInputTextBox_LostFocus; }
+ if (this.ColorModeComboBox != null) { this.ColorModeComboBox.SelectionChanged += ColorModeComboBox_SelectionChanged; }
+
+ if (this.Channel1NumberBox != null) { this.Channel1NumberBox.ValueChanged += ChannelNumberBox_ValueChanged; }
+ if (this.Channel2NumberBox != null) { this.Channel2NumberBox.ValueChanged += ChannelNumberBox_ValueChanged; }
+ if (this.Channel3NumberBox != null) { this.Channel3NumberBox.ValueChanged += ChannelNumberBox_ValueChanged; }
+ if (this.AlphaChannelNumberBox != null) { this.AlphaChannelNumberBox.ValueChanged += ChannelNumberBox_ValueChanged; }
+
+ if (this.Channel1Slider != null) { this.Channel1Slider.ValueChanged += ChannelSlider_ValueChanged; }
+ if (this.Channel2Slider != null) { this.Channel2Slider.ValueChanged += ChannelSlider_ValueChanged; }
+ if (this.Channel3Slider != null) { this.Channel3Slider.ValueChanged += ChannelSlider_ValueChanged; }
+ if (this.AlphaChannelSlider != null) { this.AlphaChannelSlider.ValueChanged += ChannelSlider_ValueChanged; }
+ if (this.ColorSpectrumAlphaSlider != null) { this.ColorSpectrumAlphaSlider.ValueChanged += ChannelSlider_ValueChanged; }
+ if (this.ColorSpectrumThirdDimensionSlider != null) { this.ColorSpectrumThirdDimensionSlider.ValueChanged += ChannelSlider_ValueChanged; }
+
+ if (this.Channel1Slider != null) { this.Channel1Slider.Loaded += ChannelSlider_Loaded; }
+ if (this.Channel2Slider != null) { this.Channel2Slider.Loaded += ChannelSlider_Loaded; }
+ if (this.Channel3Slider != null) { this.Channel3Slider.Loaded += ChannelSlider_Loaded; }
+ if (this.AlphaChannelSlider != null) { this.AlphaChannelSlider.Loaded += ChannelSlider_Loaded; }
+ if (this.ColorSpectrumAlphaSlider != null) { this.ColorSpectrumAlphaSlider.Loaded += ChannelSlider_Loaded; }
+ if (this.ColorSpectrumThirdDimensionSlider != null) { this.ColorSpectrumThirdDimensionSlider.Loaded += ChannelSlider_Loaded; }
+
+ if (this.ColorPreviewer != null) { this.ColorPreviewer.ColorChangeRequested += ColorPreviewer_ColorChangeRequested; }
+
+ if (this.CheckeredBackground1Border != null) { this.CheckeredBackground1Border.Loaded += CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground2Border != null) { this.CheckeredBackground2Border.Loaded += CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground3Border != null) { this.CheckeredBackground3Border.Loaded += CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground4Border != null) { this.CheckeredBackground4Border.Loaded += CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground5Border != null) { this.CheckeredBackground5Border.Loaded += CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground6Border != null) { this.CheckeredBackground6Border.Loaded += CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground7Border != null) { this.CheckeredBackground7Border.Loaded += CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground8Border != null) { this.CheckeredBackground8Border.Loaded += CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground9Border != null) { this.CheckeredBackground9Border.Loaded += CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground10Border != null) { this.CheckeredBackground10Border.Loaded += CheckeredBackgroundBorder_Loaded; }
+
+ this.eventsConnected = true;
+ }
+ else if (connected == false &&
+ this.eventsConnected == true)
+ {
+ // Remove all events
+ if (this.ColorSpectrumControl != null) { this.ColorSpectrumControl.ColorChanged -= ColorSpectrum_ColorChanged; }
+ if (this.ColorSpectrumControl != null) { this.ColorSpectrumControl.GotFocus -= ColorSpectrum_GotFocus; }
+ if (this.HexInputTextBox != null) { this.HexInputTextBox.KeyDown -= HexInputTextBox_KeyDown; }
+ if (this.HexInputTextBox != null) { this.HexInputTextBox.LostFocus -= HexInputTextBox_LostFocus; }
+ if (this.ColorModeComboBox != null) { this.ColorModeComboBox.SelectionChanged -= ColorModeComboBox_SelectionChanged; }
+
+ if (this.Channel1NumberBox != null) { this.Channel1NumberBox.ValueChanged -= ChannelNumberBox_ValueChanged; }
+ if (this.Channel2NumberBox != null) { this.Channel2NumberBox.ValueChanged -= ChannelNumberBox_ValueChanged; }
+ if (this.Channel3NumberBox != null) { this.Channel3NumberBox.ValueChanged -= ChannelNumberBox_ValueChanged; }
+ if (this.AlphaChannelNumberBox != null) { this.AlphaChannelNumberBox.ValueChanged -= ChannelNumberBox_ValueChanged; }
+
+ if (this.Channel1Slider != null) { this.Channel1Slider.ValueChanged -= ChannelSlider_ValueChanged; }
+ if (this.Channel2Slider != null) { this.Channel2Slider.ValueChanged -= ChannelSlider_ValueChanged; }
+ if (this.Channel3Slider != null) { this.Channel3Slider.ValueChanged -= ChannelSlider_ValueChanged; }
+ if (this.AlphaChannelSlider != null) { this.AlphaChannelSlider.ValueChanged -= ChannelSlider_ValueChanged; }
+ if (this.ColorSpectrumAlphaSlider != null) { this.ColorSpectrumAlphaSlider.ValueChanged -= ChannelSlider_ValueChanged; }
+ if (this.ColorSpectrumThirdDimensionSlider != null) { this.ColorSpectrumThirdDimensionSlider.ValueChanged -= ChannelSlider_ValueChanged; }
+
+ if (this.Channel1Slider != null) { this.Channel1Slider.Loaded -= ChannelSlider_Loaded; }
+ if (this.Channel2Slider != null) { this.Channel2Slider.Loaded -= ChannelSlider_Loaded; }
+ if (this.Channel3Slider != null) { this.Channel3Slider.Loaded -= ChannelSlider_Loaded; }
+ if (this.AlphaChannelSlider != null) { this.AlphaChannelSlider.Loaded -= ChannelSlider_Loaded; }
+ if (this.ColorSpectrumAlphaSlider != null) { this.ColorSpectrumAlphaSlider.Loaded -= ChannelSlider_Loaded; }
+ if (this.ColorSpectrumThirdDimensionSlider != null) { this.ColorSpectrumThirdDimensionSlider.Loaded -= ChannelSlider_Loaded; }
+
+ if (this.ColorPreviewer != null) { this.ColorPreviewer.ColorChangeRequested -= ColorPreviewer_ColorChangeRequested; }
+
+ if (this.CheckeredBackground1Border != null) { this.CheckeredBackground1Border.Loaded -= CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground2Border != null) { this.CheckeredBackground2Border.Loaded -= CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground3Border != null) { this.CheckeredBackground3Border.Loaded -= CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground4Border != null) { this.CheckeredBackground4Border.Loaded -= CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground5Border != null) { this.CheckeredBackground5Border.Loaded -= CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground6Border != null) { this.CheckeredBackground6Border.Loaded -= CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground7Border != null) { this.CheckeredBackground7Border.Loaded -= CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground8Border != null) { this.CheckeredBackground8Border.Loaded -= CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground9Border != null) { this.CheckeredBackground9Border.Loaded -= CheckeredBackgroundBorder_Loaded; }
+ if (this.CheckeredBackground10Border != null) { this.CheckeredBackground10Border.Loaded -= CheckeredBackgroundBorder_Loaded; }
+
+ this.eventsConnected = false;
+ }
+
+ return;
+ }
+
+
+ ///
+ /// Updates all visual states based on current control properties.
+ ///
+ /// Whether transitions should occur when changing states.
+ private void UpdateVisualState(bool useTransitions = true)
+ {
+ VisualStateManager.GoToState(this, this.IsEnabled ? "Normal" : "Disabled", useTransitions);
+ VisualStateManager.GoToState(this, this.GetActiveColorRepresentation() == ColorRepresentation.Hsva ? "HsvSelected" : "RgbSelected", useTransitions);
+ VisualStateManager.GoToState(this, this.IsColorPaletteVisible ? "ColorPaletteVisible" : "ColorPaletteCollapsed", useTransitions);
+
+ // Check if only a single vie is selected and hide the Segmented control
+ VisualStateManager.GoToState(this, (Truth(IsColorPaletteVisible, IsColorSpectrumVisible, IsColorChannelTextInputVisible) <= 1) ? "ColorPanelSelectorCollapsed" : "ColorPanelSelectorVisible", useTransitions);
+ }
+
+ public static int Truth(params bool[] booleans)
+ {
+ return booleans.Count(b => b);
+ }
+
+ ///
+ /// Gets the active representation of the color: HSV or RGB.
+ ///
+ private ColorRepresentation GetActiveColorRepresentation()
+ {
+ // If the HSV representation control is missing for whatever reason,
+ // the default will be RGB
+
+ if (this.ColorModeComboBox != null &&
+ this.ColorModeComboBox.SelectedIndex == 1)
+ {
+ return ColorRepresentation.Hsva;
+ }
+
+ return ColorRepresentation.Rgba;
+ }
+
+ ///
+ /// Sets the active color representation in the UI controls.
+ ///
+ /// The color representation to set.
+ /// Setting to null (the default) will attempt to keep the current state.
+ private void SetActiveColorRepresentation(ColorRepresentation? colorRepresentation = null)
+ {
+ bool eventsDisconnectedByMethod = false;
+
+ if (colorRepresentation == null)
+ {
+ // Use the control's current value
+ colorRepresentation = this.GetActiveColorRepresentation();
+ }
+
+ // Disable events during the update
+ if (this.eventsConnected)
+ {
+ this.ConnectEvents(false);
+ eventsDisconnectedByMethod = true;
+ }
+
+ // Sync the UI controls and visual state
+ // The default is always RGBA
+ if (colorRepresentation == ColorRepresentation.Hsva)
+ {
+ this.ColorModeComboBox.SelectedIndex = 1;
+ }
+ else
+ {
+ this.ColorModeComboBox.SelectedIndex = 0;
+ }
+
+ this.UpdateVisualState(false);
+
+ if (eventsDisconnectedByMethod)
+ {
+ this.ConnectEvents(true);
+ }
+
+ return;
+ }
+
+ ///
+ /// Gets the active third dimension in the color spectrum: Hue, Saturation or Value.
+ ///
+ private ColorChannel GetActiveColorSpectrumThirdDimension()
+ {
+ switch (this.ColorSpectrumComponents)
+ {
+ case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.SaturationValue:
+ case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.ValueSaturation:
+ {
+ // Hue
+ return ColorChannel.Channel1;
+ }
+
+ case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.HueValue:
+ case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.ValueHue:
+ {
+ // Saturation
+ return ColorChannel.Channel2;
+ }
+
+ case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.HueSaturation:
+ case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.SaturationHue:
+ {
+ // Value
+ return ColorChannel.Channel3;
+ }
+ }
+
+ return ColorChannel.Alpha; // Error, should never get here
+ }
+
+ ///
+ /// Declares a new color to set to the control.
+ /// Application of this color will be scheduled to avoid overly rapid updates.
+ ///
+ /// The new color to set to the control.
+ private void ScheduleColorUpdate(Color newColor)
+ {
+ // Coerce the value as needed
+ if (this.IsAlphaEnabled == false)
+ {
+ newColor = new Color()
+ {
+ R = newColor.R,
+ G = newColor.G,
+ B = newColor.B,
+ A = 255
+ };
+ }
+
+ this.updatedRgbColor = newColor;
+
+ return;
+ }
+
+ ///
+ /// Updates the color values in all editing controls to match the current color.
+ ///
+ private void UpdateColorControlValues()
+ {
+ bool eventsDisconnectedByMethod = false;
+ Color rgbColor = this.Color;
+ HsvColor hsvColor;
+
+ if (this.isInitialized)
+ {
+ // Disable events during the update
+ if (this.eventsConnected)
+ {
+ this.ConnectEvents(false);
+ eventsDisconnectedByMethod = true;
+ }
+
+ if (this.HexInputTextBox != null)
+ {
+ if (this.IsAlphaEnabled)
+ {
+ // Remove only the "#" sign
+ this.HexInputTextBox.Text = rgbColor.ToHex().Replace("#", string.Empty);
+ }
+ else
+ {
+ // Remove the "#" sign and alpha hex
+ this.HexInputTextBox.Text = rgbColor.ToHex().Replace("#", string.Empty).Substring(2);
+ }
+ }
+
+ // Regardless of the active color representation, the spectrum is always HSV
+ // Therefore, always calculate HSV color here
+ // Warning: Always maintain/use HSV information in the saved HSV color
+ // This avoids loss of precision and drift caused by continuously converting to/from RGB
+ if (this.savedHsvColor == null)
+ {
+ hsvColor = rgbColor.ToHsv();
+
+ // Round the channels, be sure rounding matches with the scaling next
+ // Rounding of SVA requires at MINIMUM 2 decimal places
+ int decimals = 0;
+ hsvColor = new HsvColor()
+ {
+ H = Math.Round(hsvColor.H, decimals),
+ S = Math.Round(hsvColor.S, 2 + decimals),
+ V = Math.Round(hsvColor.V, 2 + decimals),
+ A = Math.Round(hsvColor.A, 2 + decimals)
+ };
+
+ // Must update HSV color
+ this.savedHsvColor = hsvColor;
+ this.savedHsvColorRgbEquivalent = rgbColor;
+ }
+ else
+ {
+ hsvColor = this.savedHsvColor.Value;
+ }
+
+ // Update the color spectrum
+ // Remember the spectrum is always HSV and must be updated as such to avoid
+ // conversion errors
+ if (this.ColorSpectrumControl != null)
+ {
+ this.ColorSpectrumControl.HsvColor = new System.Numerics.Vector4()
+ {
+ X = Convert.ToSingle(hsvColor.H),
+ Y = Convert.ToSingle(hsvColor.S),
+ Z = Convert.ToSingle(hsvColor.V),
+ W = Convert.ToSingle(hsvColor.A)
+ };
+ }
+
+ // Update the color spectrum third dimension channel
+ if (this.ColorSpectrumThirdDimensionSlider != null)
+ {
+ // Convert the channels into a usable range for the user
+ double hue = hsvColor.H;
+ double staturation = hsvColor.S * 100;
+ double value = hsvColor.V * 100;
+
+ switch (this.GetActiveColorSpectrumThirdDimension())
+ {
+ case ColorChannel.Channel1:
+ {
+ // Hue
+ this.ColorSpectrumThirdDimensionSlider.Minimum = 0;
+ this.ColorSpectrumThirdDimensionSlider.Maximum = 360;
+ this.ColorSpectrumThirdDimensionSlider.Value = hue;
+ break;
+ }
+
+ case ColorChannel.Channel2:
+ {
+ // Saturation
+ this.ColorSpectrumThirdDimensionSlider.Minimum = 0;
+ this.ColorSpectrumThirdDimensionSlider.Maximum = 100;
+ this.ColorSpectrumThirdDimensionSlider.Value = staturation;
+ break;
+ }
+
+ case ColorChannel.Channel3:
+ {
+ // Value
+ this.ColorSpectrumThirdDimensionSlider.Minimum = 0;
+ this.ColorSpectrumThirdDimensionSlider.Maximum = 100;
+ this.ColorSpectrumThirdDimensionSlider.Value = value;
+ break;
+ }
+ }
+ }
+
+ // Update the preview color
+ if (this.ColorPreviewer != null)
+ {
+ this.ColorPreviewer.HsvColor = hsvColor;
+ }
+
+ // Update all other color channels
+ if (this.GetActiveColorRepresentation() == ColorRepresentation.Hsva)
+ {
+ // Convert the channels into a usable range for the user
+ double hue = hsvColor.H;
+ double staturation = hsvColor.S * 100;
+ double value = hsvColor.V * 100;
+ double alpha = hsvColor.A * 100;
+
+ // Hue
+ if (this.Channel1NumberBox != null)
+ {
+ this.Channel1NumberBox.Minimum = 0;
+ this.Channel1NumberBox.Maximum = 360;
+ this.Channel1NumberBox.Value = hue;
+ }
+
+ if (this.Channel1Slider != null)
+ {
+ this.Channel1Slider.Minimum = 0;
+ this.Channel1Slider.Maximum = 360;
+ this.Channel1Slider.Value = hue;
+ }
+
+ // Saturation
+ if (this.Channel2NumberBox != null)
+ {
+ this.Channel2NumberBox.Minimum = 0;
+ this.Channel2NumberBox.Maximum = 100;
+ this.Channel2NumberBox.Value = staturation;
+ }
+
+ if (this.Channel2Slider != null)
+ {
+ this.Channel2Slider.Minimum = 0;
+ this.Channel2Slider.Maximum = 100;
+ this.Channel2Slider.Value = staturation;
+ }
+
+ // Value
+ if (this.Channel3NumberBox != null)
+ {
+ this.Channel3NumberBox.Minimum = 0;
+ this.Channel3NumberBox.Maximum = 100;
+ this.Channel3NumberBox.Value = value;
+ }
+
+ if (this.Channel3Slider != null)
+ {
+ this.Channel3Slider.Minimum = 0;
+ this.Channel3Slider.Maximum = 100;
+ this.Channel3Slider.Value = value;
+ }
+
+ // Alpha
+ if (this.AlphaChannelNumberBox != null)
+ {
+ this.AlphaChannelNumberBox.Minimum = 0;
+ this.AlphaChannelNumberBox.Maximum = 100;
+ this.AlphaChannelNumberBox.Value = alpha;
+ }
+
+ if (this.AlphaChannelSlider != null)
+ {
+ this.AlphaChannelSlider.Minimum = 0;
+ this.AlphaChannelSlider.Maximum = 100;
+ this.AlphaChannelSlider.Value = alpha;
+ }
+
+ // Color spectrum alpha
+ if (this.ColorSpectrumAlphaSlider != null)
+ {
+ this.ColorSpectrumAlphaSlider.Minimum = 0;
+ this.ColorSpectrumAlphaSlider.Maximum = 100;
+ this.ColorSpectrumAlphaSlider.Value = alpha;
+ }
+ }
+ else
+ {
+ // Red
+ if (this.Channel1NumberBox != null)
+ {
+ this.Channel1NumberBox.Minimum = 0;
+ this.Channel1NumberBox.Maximum = 255;
+ this.Channel1NumberBox.Value = Convert.ToDouble(rgbColor.R);
+ }
+
+ if (this.Channel1Slider != null)
+ {
+ this.Channel1Slider.Minimum = 0;
+ this.Channel1Slider.Maximum = 255;
+ this.Channel1Slider.Value = Convert.ToDouble(rgbColor.R);
+ }
+
+ // Green
+ if (this.Channel2NumberBox != null)
+ {
+ this.Channel2NumberBox.Minimum = 0;
+ this.Channel2NumberBox.Maximum = 255;
+ this.Channel2NumberBox.Value = Convert.ToDouble(rgbColor.G);
+ }
+
+ if (this.Channel2Slider != null)
+ {
+ this.Channel2Slider.Minimum = 0;
+ this.Channel2Slider.Maximum = 255;
+ this.Channel2Slider.Value = Convert.ToDouble(rgbColor.G);
+ }
+
+ // Blue
+ if (this.Channel3NumberBox != null)
+ {
+ this.Channel3NumberBox.Minimum = 0;
+ this.Channel3NumberBox.Maximum = 255;
+ this.Channel3NumberBox.Value = Convert.ToDouble(rgbColor.B);
+ }
+
+ if (this.Channel3Slider != null)
+ {
+ this.Channel3Slider.Minimum = 0;
+ this.Channel3Slider.Maximum = 255;
+ this.Channel3Slider.Value = Convert.ToDouble(rgbColor.B);
+ }
+
+ // Alpha
+ if (this.AlphaChannelNumberBox != null)
+ {
+ this.AlphaChannelNumberBox.Minimum = 0;
+ this.AlphaChannelNumberBox.Maximum = 255;
+ this.AlphaChannelNumberBox.Value = Convert.ToDouble(rgbColor.A);
+ }
+
+ if (this.AlphaChannelSlider != null)
+ {
+ this.AlphaChannelSlider.Minimum = 0;
+ this.AlphaChannelSlider.Maximum = 255;
+ this.AlphaChannelSlider.Value = Convert.ToDouble(rgbColor.A);
+ }
+
+ // Color spectrum alpha
+ if (this.ColorSpectrumAlphaSlider != null)
+ {
+ this.ColorSpectrumAlphaSlider.Minimum = 0;
+ this.ColorSpectrumAlphaSlider.Maximum = 255;
+ this.ColorSpectrumAlphaSlider.Value = Convert.ToDouble(rgbColor.A);
+ }
+ }
+
+ if (eventsDisconnectedByMethod)
+ {
+ this.ConnectEvents(true);
+ }
+ }
+
+ return;
+ }
+
+ ///
+ /// Sets a new color channel value to the current color.
+ /// Only the specified color channel will be modified.
+ ///
+ /// The color representation of the given channel.
+ /// The specified color channel to modify.
+ /// The new color channel value.
+ private void SetColorChannel(
+ ColorRepresentation colorRepresentation,
+ ColorChannel channel,
+ double newValue)
+ {
+ Color oldRgbColor = this.Color;
+ Color newRgbColor;
+ HsvColor oldHsvColor;
+
+ if (colorRepresentation == ColorRepresentation.Hsva)
+ {
+ // Warning: Always maintain/use HSV information in the saved HSV color
+ // This avoids loss of precision and drift caused by continuously converting to/from RGB
+ if (this.savedHsvColor == null)
+ {
+ oldHsvColor = oldRgbColor.ToHsv();
+ }
+ else
+ {
+ oldHsvColor = this.savedHsvColor.Value;
+ }
+
+ double hue = oldHsvColor.H;
+ double saturation = oldHsvColor.S;
+ double value = oldHsvColor.V;
+ double alpha = oldHsvColor.A;
+
+ switch (channel)
+ {
+ case ColorChannel.Channel1:
+ {
+ hue = Math.Clamp(double.IsNaN(newValue) ? 0 : newValue, 0, 360);
+ break;
+ }
+
+ case ColorChannel.Channel2:
+ {
+ saturation = Math.Clamp((double.IsNaN(newValue) ? 0 : newValue) / 100, 0, 1);
+ break;
+ }
+
+ case ColorChannel.Channel3:
+ {
+ value = Math.Clamp((double.IsNaN(newValue) ? 0 : newValue) / 100, 0, 1);
+ break;
+ }
+
+ case ColorChannel.Alpha:
+ {
+ // Unlike color channels, default to no transparency
+ alpha = Math.Clamp((double.IsNaN(newValue) ? 100 : newValue) / 100, 0, 1);
+ break;
+ }
+ }
+
+ newRgbColor = Helpers.ColorHelper.FromHsv(
+ hue,
+ saturation,
+ value,
+ alpha);
+
+ // Must update HSV color
+ this.savedHsvColor = new HsvColor()
+ {
+ H = hue,
+ S = saturation,
+ V = value,
+ A = alpha
+ };
+ this.savedHsvColorRgbEquivalent = newRgbColor;
+ }
+ else
+ {
+ byte red = oldRgbColor.R;
+ byte green = oldRgbColor.G;
+ byte blue = oldRgbColor.B;
+ byte alpha = oldRgbColor.A;
+
+ switch (channel)
+ {
+ case ColorChannel.Channel1:
+ {
+ red = Convert.ToByte(Math.Clamp(double.IsNaN(newValue) ? 0 : newValue, 0, 255));
+ break;
+ }
+
+ case ColorChannel.Channel2:
+ {
+ green = Convert.ToByte(Math.Clamp(double.IsNaN(newValue) ? 0 : newValue, 0, 255));
+ break;
+ }
+
+ case ColorChannel.Channel3:
+ {
+ blue = Convert.ToByte(Math.Clamp(double.IsNaN(newValue) ? 0 : newValue, 0, 255));
+ break;
+ }
+
+ case ColorChannel.Alpha:
+ {
+ // Unlike color channels, default to no transparency
+ alpha = Convert.ToByte(Math.Clamp(double.IsNaN(newValue) ? 255 : newValue, 0, 255));
+ break;
+ }
+ }
+
+ newRgbColor = new Color()
+ {
+ R = red,
+ G = green,
+ B = blue,
+ A = alpha
+ };
+
+ // Must clear saved HSV color
+ this.savedHsvColor = null;
+ this.savedHsvColorRgbEquivalent = null;
+ }
+
+ this.ScheduleColorUpdate(newRgbColor);
+ return;
+ }
+
+ ///
+ /// Updates all channel slider control backgrounds.
+ ///
+ private void UpdateChannelSliderBackgrounds()
+ {
+ this.UpdateChannelSliderBackground(this.Channel1Slider);
+ this.UpdateChannelSliderBackground(this.Channel2Slider);
+ this.UpdateChannelSliderBackground(this.Channel3Slider);
+ this.UpdateChannelSliderBackground(this.AlphaChannelSlider);
+ this.UpdateChannelSliderBackground(this.ColorSpectrumAlphaSlider);
+ this.UpdateChannelSliderBackground(this.ColorSpectrumThirdDimensionSlider);
+ return;
+ }
+
+ ///
+ /// Updates a specific channel slider control background.
+ ///
+ /// The color channel slider to update the background for.
+ private void UpdateChannelSliderBackground(ColorPickerSlider slider)
+ {
+ if (slider != null)
+ {
+ // Regardless of the active color representation, the sliders always use HSV
+ // Therefore, always calculate HSV color here
+ // Warning: Always maintain/use HSV information in the saved HSV color
+ // This avoids loss of precision and drift caused by continuously converting to/from RGB
+ if (this.savedHsvColor == null)
+ {
+ var rgbColor = this.Color;
+
+ // Must update HSV color
+ this.savedHsvColor = rgbColor.ToHsv();
+ this.savedHsvColorRgbEquivalent = rgbColor;
+ }
+
+ slider.IsAutoUpdatingEnabled = false;
+
+ if (object.ReferenceEquals(slider, this.Channel1Slider))
+ {
+ slider.ColorChannel = ColorChannel.Channel1;
+ slider.ColorRepresentation = this.GetActiveColorRepresentation();
+ }
+ else if (object.ReferenceEquals(slider, this.Channel2Slider))
+ {
+ slider.ColorChannel = ColorChannel.Channel2;
+ slider.ColorRepresentation = this.GetActiveColorRepresentation();
+ }
+ else if (object.ReferenceEquals(slider, this.Channel3Slider))
+ {
+ slider.ColorChannel = ColorChannel.Channel3;
+ slider.ColorRepresentation = this.GetActiveColorRepresentation();
+ }
+ else if (object.ReferenceEquals(slider, this.AlphaChannelSlider))
+ {
+ slider.ColorChannel = ColorChannel.Alpha;
+ slider.ColorRepresentation = this.GetActiveColorRepresentation();
+ }
+ else if (object.ReferenceEquals(slider, this.ColorSpectrumAlphaSlider))
+ {
+ slider.ColorChannel = ColorChannel.Alpha;
+ slider.ColorRepresentation = this.GetActiveColorRepresentation();
+ }
+ else if (object.ReferenceEquals(slider, this.ColorSpectrumThirdDimensionSlider))
+ {
+ slider.ColorChannel = this.GetActiveColorSpectrumThirdDimension();
+ slider.ColorRepresentation = ColorRepresentation.Hsva; // Always HSV
+ }
+
+ slider.HsvColor = this.savedHsvColor.Value;
+ slider.UpdateColors();
+ }
+
+ return;
+ }
+
+ ///
+ /// Sets the default color palette to the control.
+ ///
+ private void SetDefaultPalette()
+ {
+ this.CustomPalette = new FluentColorPalette();
+
+ return;
+ }
+
+ ///
+ /// Validates and updates the current 'tab' or 'panel' selection.
+ /// If the currently selected tab is collapsed, the next visible tab will be selected.
+ ///
+ private void ValidateSelectedPanel()
+ {
+ object? selectedItem = null;
+
+ if (this.ColorPanelSelector != null)
+ {
+ if (this.ColorPanelSelector.SelectedItem == null &&
+ this.ColorPanelSelector.Items.Count > 0)
+ {
+ // As a failsafe, forcefully select the first item
+ selectedItem = this.ColorPanelSelector.Items[0];
+ }
+ else
+ {
+ selectedItem = this.ColorPanelSelector.SelectedItem;
+ }
+
+ if (selectedItem is UIElement selectedElement &&
+ selectedElement.Visibility == Visibility.Collapsed)
+ {
+ // Select the first visible item instead
+ foreach (object item in this.ColorPanelSelector.Items)
+ {
+ if (item is UIElement element &&
+ element.Visibility == Visibility.Visible)
+ {
+ selectedItem = item;
+ break;
+ }
+ }
+ }
+
+ this.ColorPanelSelector.SelectedItem = selectedItem;
+ }
+
+ return;
+ }
+ private void OnPanelVisibilityChanged(DependencyObject sender, DependencyProperty dp)
+ {
+ this.UpdateVisualState(false);
+ this.ValidateSelectedPanel();
+ }
+
+ private void OnDependencyPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
+ {
+ if (sender is DependencyObject senderControl)
+ {
+ /* Note: ColorProperty is defined in the base class and cannot be used here
+ * See the OnColorChanged callback below
+ */
+
+ if (object.ReferenceEquals(args.Property, CustomPaletteProperty))
+ {
+ IColorPalette palette = this.CustomPalette;
+
+ if (palette != null)
+ {
+ this.CustomPaletteColumnCount = palette.ColorCount;
+ this.CustomPaletteColors.Clear();
+
+ for (int shadeIndex = 0; shadeIndex < palette.ShadeCount; shadeIndex++)
+ {
+ for (int colorIndex = 0; colorIndex < palette.ColorCount; colorIndex++)
+ {
+ this.CustomPaletteColors.Add(palette.GetColor(colorIndex, shadeIndex));
+ }
+ }
+ }
+ }
+ else if (object.ReferenceEquals(args.Property, IsColorPaletteVisibleProperty))
+ {
+ this.UpdateVisualState(false);
+ this.ValidateSelectedPanel();
+ }
+ }
+
+ return;
+ }
+
+ /***************************************************************************************
+ *
+ * Color Update Timer
+ *
+ ***************************************************************************************/
+
+ private void StartDispatcherQueueTimer()
+ {
+ this.dispatcherQueueTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
+ this.dispatcherQueueTimer.Interval = new TimeSpan(0, 0, 0, 0, ColorUpdateInterval);
+ this.dispatcherQueueTimer.Tick += DispatcherQueueTimer_Tick;
+ this.dispatcherQueueTimer.Start();
+
+ return;
+ }
+
+ private void StopDispatcherQueueTimer()
+ {
+ if (this.dispatcherQueueTimer != null)
+ {
+ this.dispatcherQueueTimer.Stop();
+ }
+
+ return;
+ }
+
+ private void DispatcherQueueTimer_Tick(object sender, object e)
+ {
+ if (this.updatedRgbColor != null)
+ {
+ var newColor = this.updatedRgbColor.Value;
+
+ // Clear first to avoid timing issues if it takes longer than the timer interval to set the new color
+ this.updatedRgbColor = null;
+
+ // An equality check here is important
+ // Without it, OnColorChanged would continuously be invoked and preserveHsvColor overwritten when not wanted
+ if (object.Equals(newColor, this.GetValue(ColorProperty)) == false)
+ {
+ // Disable events here so the color update isn't repeated as other controls in the UI are updated through binding.
+ // For example, the Spectrum should be bound to Color, as soon as Color is changed here the Spectrum is updated.
+ // Then however, the ColorSpectrum.ColorChanged event would fire which would schedule a new color update --
+ // with the same color. This causes several problems:
+ // 1. Layout cycle that may crash the app
+ // 2. A performance hit recalculating for no reason
+ // 3. preserveHsvColor gets overwritten unexpectedly by the ColorChanged handler
+ this.ConnectEvents(false);
+ this.SetValue(ColorProperty, newColor);
+ this.ConnectEvents(true);
+ }
+ }
+
+ return;
+ }
+
+ /***************************************************************************************
+ *
+ * Callbacks
+ *
+ ***************************************************************************************/
+
+ ///
+ /// Callback for when the dependency property value changes.
+ ///
+ private void OnColorChanged(DependencyObject d, DependencyProperty e)
+ {
+ // TODO: Coerce the value if Alpha is disabled, is this handled in the base ColorPicker?
+ if ((this.savedHsvColor != null) &&
+ (object.Equals(d.GetValue(e), this.savedHsvColorRgbEquivalent) == false))
+ {
+ // The color was updated from an unknown source
+ // The RGB and HSV colors are no longer in sync so the HSV color must be cleared
+ this.savedHsvColor = null;
+ this.savedHsvColorRgbEquivalent = null;
+ }
+
+ this.UpdateColorControlValues();
+ this.UpdateChannelSliderBackgrounds();
+
+ return;
+ }
+
+ /***************************************************************************************
+ *
+ * Event Handling
+ *
+ ***************************************************************************************/
+
+ ///
+ /// Event handler for when the control has finished loaded.
+ ///
+ private void ColorPickerButton_Loaded(object sender, RoutedEventArgs e)
+ {
+ // Available but not currently used
+ return;
+ }
+
+ ///
+ /// Event handler for when a color channel slider is loaded.
+ /// This will draw an initial background.
+ ///
+ private void ChannelSlider_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is ColorPickerSlider slider)
+ {
+ this.UpdateChannelSliderBackground(slider);
+ }
+ return;
+ }
+
+ ///
+ /// Event handler to draw checkered backgrounds on-demand as controls are loaded.
+ ///
+ private async void CheckeredBackgroundBorder_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is Border border)
+ {
+ await ColorPickerRenderingHelpers.UpdateBorderBackgroundWithCheckerAsync(
+ border,
+ CheckerBackgroundColor);
+ }
+
+ }
+
+ ///
+ /// Event handler for when the list of custom palette colors is changed.
+ ///
+ private void CustomPaletteColors_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ // Available but not currently used
+ return;
+ }
+
+ ///
+ /// Event handler for when the color spectrum color is changed.
+ /// This occurs when the user presses on the spectrum to select a new color.
+ ///
+ private void ColorSpectrum_ColorChanged(ColorSpectrum sender, Microsoft.UI.Xaml.Controls.ColorChangedEventArgs args)
+ {
+ // It is OK in this case to use the RGB representation
+ this.ScheduleColorUpdate(this.ColorSpectrumControl.Color);
+ return;
+ }
+
+ ///
+ /// Event handler for when the color spectrum is focused.
+ /// This is used only to work around some bugs that cause usability problems.
+ ///
+ private void ColorSpectrum_GotFocus(object sender, RoutedEventArgs e)
+ {
+ Color rgbColor = this.ColorSpectrumControl.Color;
+
+ /* If this control has a color that is currently empty (#00000000),
+ * selecting a new color directly in the spectrum will fail. This is
+ * a bug in the color spectrum. Selecting a new color in the spectrum will
+ * keep zero for all channels (including alpha and the third dimension).
+ *
+ * In practice this means a new color cannot be selected using the spectrum
+ * until both the alpha and third dimension slider are raised above zero.
+ * This is extremely user unfriendly and must be corrected as best as possible.
+ *
+ * In order to work around this, detect when the color spectrum has selected
+ * a new color and then automatically set the alpha and third dimension
+ * channel to maximum. However, the color spectrum has a second bug, the
+ * ColorChanged event is never raised if the color is empty. This prevents
+ * automatically setting the other channels where it normally should be done
+ * (in the ColorChanged event).
+ *
+ * In order to work around this second bug, the GotFocus event is used
+ * to detect when the spectrum is engaged by the user. It's somewhat equivalent
+ * to ColorChanged for this purpose. Then when the GotFocus event is fired
+ * set the alpha and third channel values to maximum. The problem here is that
+ * the GotFocus event does not have access to the new color that was selected
+ * in the spectrum. It is not available due to the afore mentioned bug or due to
+ * timing. This means the best that can be done is to just set a 'neutral'
+ * color such as white.
+ *
+ * There is still a small usability issue with this as it requires two
+ * presses to set a color. That's far better than having to slide up both
+ * sliders though.
+ *
+ * 1. If the color is empty, the first press on the spectrum will set white
+ * and ignore the pressed color on the spectrum
+ * 2. The second press on the spectrum will be correctly handled.
+ *
+ */
+
+ // In the future Color.IsEmpty will hopefully be added to UWP
+ if (IsColorEmpty(rgbColor))
+ {
+ /* The following code may be used in the future if ever the selected color is available
+
+ Color newColor = this.ColorSpectrum.Color;
+ HsvColor newHsvColor = newColor.ToHsv();
+
+ switch (this.GetActiveColorSpectrumThirdDimension())
+ {
+ case ColorChannel.Channel1:
+ {
+ newColor = Microsoft.Toolkit.Uwp.Helpers.ColorHelper.FromHsv
+ (
+ 360.0,
+ newHsvColor.S,
+ newHsvColor.V,
+ 100.0
+ );
+ break;
+ }
+
+ case ColorChannel.Channel2:
+ {
+ newColor = Microsoft.Toolkit.Uwp.Helpers.ColorHelper.FromHsv
+ (
+ newHsvColor.H,
+ 100.0,
+ newHsvColor.V,
+ 100.0
+ );
+ break;
+ }
+
+ case ColorChannel.Channel3:
+ {
+ newColor = Microsoft.Toolkit.Uwp.Helpers.ColorHelper.FromHsv
+ (
+ newHsvColor.H,
+ newHsvColor.S,
+ 100.0,
+ 100.0
+ );
+ break;
+ }
+ }
+ */
+
+ this.ScheduleColorUpdate(Colors.White);
+ }
+ else if (rgbColor.A == 0x00)
+ {
+ // As an additional usability improvement, reset alpha to maximum when the spectrum is used.
+ // The color spectrum has no alpha channel and it is much more intuitive to do this for the user
+ // especially if the picker was initially set with Colors.Transparent.
+ this.ScheduleColorUpdate(Color.FromArgb(0xFF, rgbColor.R, rgbColor.G, rgbColor.B));
+ }
+
+ return;
+ }
+
+ ///
+ /// Event handler for when the selected color representation changes.
+ /// This will convert between RGB and HSV.
+ ///
+ private void ColorModeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (this.ColorModeComboBox.SelectedIndex == 1)
+ {
+ this.SetActiveColorRepresentation(ColorRepresentation.Hsva);
+ }
+ else
+ {
+ this.SetActiveColorRepresentation(ColorRepresentation.Rgba);
+ }
+
+ this.UpdateColorControlValues();
+ this.UpdateChannelSliderBackgrounds();
+
+ return;
+ }
+
+ ///
+ /// Event handler for when the color previewer requests a new color.
+ ///
+ private void ColorPreviewer_ColorChangeRequested(object? sender, HsvColor hsvColor)
+ {
+ Color rgbColor = Helpers.ColorHelper.FromHsv(hsvColor.H, hsvColor.S, hsvColor.V, hsvColor.A);
+
+ // Regardless of the active color model, the previewer always uses HSV
+ // Therefore, always calculate HSV color here
+ // Warning: Always maintain/use HSV information in the saved HSV color
+ // This avoids loss of precision and drift caused by continuously converting to/from RGB
+ this.savedHsvColor = hsvColor;
+ this.savedHsvColorRgbEquivalent = rgbColor;
+
+ this.ScheduleColorUpdate(rgbColor);
+
+ return;
+ }
+
+ ///
+ /// Event handler for when a key is pressed within the Hex RGB value TextBox.
+ /// This is used to trigger a re-evaluation of the color based on the TextBox value.
+ ///
+ private void HexInputTextBox_KeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ if (e.Key == Windows.System.VirtualKey.Enter)
+ {
+ try
+ {
+ ColorToHexConverter converter = new ColorToHexConverter();
+ this.Color = (Color)converter.ConvertBack(((TextBox)sender).Text, typeof(TextBox), null, null);
+ }
+ catch
+ {
+ // Reset hex value
+ this.UpdateColorControlValues();
+ this.UpdateChannelSliderBackgrounds();
+ }
+ }
+
+ return;
+ }
+
+ ///
+ /// Event handler for when the Hex RGB value TextBox looses focus.
+ /// This is used to trigger a re-evaluation of the color based on the TextBox value.
+ ///
+ private void HexInputTextBox_LostFocus(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ ColorToHexConverter converter = new ColorToHexConverter();
+ this.Color = (Color)converter.ConvertBack(((TextBox)sender).Text, typeof(TextBox), null, null);
+ }
+ catch
+ {
+ // Reset hex value
+ this.UpdateColorControlValues();
+ this.UpdateChannelSliderBackgrounds();
+ }
+
+ return;
+ }
+
+ ///
+ /// Event handler for when the value within one of the channel NumberBoxes is changed.
+ ///
+ private void ChannelNumberBox_ValueChanged(NumberBox sender, NumberBoxValueChangedEventArgs args)
+ {
+ double senderValue = sender.Value;
+
+ if (object.ReferenceEquals(sender, this.Channel1NumberBox))
+ {
+ this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel1, senderValue);
+ }
+ else if (object.ReferenceEquals(sender, this.Channel2NumberBox))
+ {
+ this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel2, senderValue);
+ }
+ else if (object.ReferenceEquals(sender, this.Channel3NumberBox))
+ {
+ this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel3, senderValue);
+ }
+ else if (object.ReferenceEquals(sender, this.AlphaChannelNumberBox))
+ {
+ this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Alpha, senderValue);
+ }
+
+ return;
+ }
+
+ ///
+ /// Event handler for when the value within one of the channel Sliders is changed.
+ ///
+ private void ChannelSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
+ {
+ double senderValue = (sender as Slider)?.Value ?? double.NaN;
+
+ if (object.ReferenceEquals(sender, this.Channel1Slider))
+ {
+ this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel1, senderValue);
+ }
+ else if (object.ReferenceEquals(sender, this.Channel2Slider))
+ {
+ this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel2, senderValue);
+ }
+ else if (object.ReferenceEquals(sender, this.Channel3Slider))
+ {
+ this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel3, senderValue);
+ }
+ else if (object.ReferenceEquals(sender, this.AlphaChannelSlider))
+ {
+ this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Alpha, senderValue);
+ }
+ else if (object.ReferenceEquals(sender, this.ColorSpectrumAlphaSlider))
+ {
+ this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Alpha, senderValue);
+ }
+ else if (object.ReferenceEquals(sender, this.ColorSpectrumThirdDimensionSlider))
+ {
+ // Regardless of the active color representation, the spectrum is always HSV
+ this.SetColorChannel(ColorRepresentation.Hsva, this.GetActiveColorSpectrumThirdDimension(), senderValue);
+ }
+
+ return;
+ }
+}
diff --git a/components/ColorPicker/src/ColorPicker.xaml b/components/ColorPicker/src/ColorPicker.xaml
new file mode 100644
index 00000000..284eb7b2
--- /dev/null
+++ b/components/ColorPicker/src/ColorPicker.xaml
@@ -0,0 +1,987 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ RGB
+ HSV
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Visible
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/ColorPicker/src/ColorPickerButton.cs b/components/ColorPicker/src/ColorPickerButton.cs
new file mode 100644
index 00000000..e1a2e0f5
--- /dev/null
+++ b/components/ColorPicker/src/ColorPickerButton.cs
@@ -0,0 +1,164 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Windows.UI;
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// A which displays a color as its Content and it's Flyout is a .
+///
+[TemplatePart(Name = nameof(CheckeredBackgroundBorder), Type = typeof(Border))]
+public partial class ColorPickerButton : DropDownButton
+{
+ ///
+ /// Gets the instances contained by the .
+ ///
+ public ColorPicker ColorPicker { get; private set; }
+
+ ///
+ /// Gets or sets the for the control used in the button.
+ ///
+ public Style ColorPickerStyle
+ {
+ get
+ {
+ return (Style)GetValue(ColorPickerStyleProperty);
+ }
+
+ set
+ {
+ SetValue(ColorPickerStyleProperty, value);
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ColorPickerStyleProperty = DependencyProperty.Register(nameof(ColorPickerStyle), typeof(Style), typeof(ColorPickerButton), new PropertyMetadata(default(Style)));
+
+ ///
+ /// Gets or sets the for the used within the of the .
+ ///
+ public Style FlyoutPresenterStyle
+ {
+ get
+ {
+ return (Style)GetValue(FlyoutPresenterStyleProperty);
+ }
+
+ set
+ {
+ SetValue(FlyoutPresenterStyleProperty, value);
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty FlyoutPresenterStyleProperty = DependencyProperty.Register(nameof(FlyoutPresenterStyle), typeof(Style), typeof(ColorPickerButton), new PropertyMetadata(default(Style)));
+
+#pragma warning disable CS0419 // Ambiguous reference in cref attribute
+ ///
+ /// Gets or sets the selected the user has picked from the .
+ ///
+#pragma warning restore CS0419 // Ambiguous reference in cref attribute
+ public Color SelectedColor
+ {
+ get { return (Color)GetValue(SelectedColorProperty); }
+ set { SetValue(SelectedColorProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty SelectedColorProperty =
+ DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ColorPickerButton), new PropertyMetadata(null, new PropertyChangedCallback(SelectedColorChanged)));
+
+#pragma warning disable SA1306 // Field names should begin with lower-case letter
+ //// Template Parts
+ private Border? CheckeredBackgroundBorder;
+#pragma warning restore SA1306 // Field names should begin with lower-case letter
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
+ public ColorPickerButton()
+#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
+ {
+ this.DefaultStyleKey = typeof(ColorPickerButton);
+
+ // Workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/3502
+ this.DefaultStyleResourceUri = new Uri("ms-appx:///CommunityToolkit.WinUI.Controls.ColorPicker/Themes/Generic.xaml");
+ }
+
+ ///
+ protected override void OnApplyTemplate()
+ {
+ if (ColorPicker != null)
+ {
+ ColorPicker.ColorChanged -= ColorPicker_ColorChanged;
+ }
+
+ base.OnApplyTemplate();
+
+ if (ColorPickerStyle != null)
+ {
+ ColorPicker = new ColorPicker() { Style = ColorPickerStyle };
+ }
+ else
+ {
+ ColorPicker = new ColorPicker();
+ }
+
+ ColorPicker.Color = SelectedColor;
+ ColorPicker.ColorChanged += ColorPicker_ColorChanged;
+
+ if (Flyout == null)
+ {
+ Flyout = new Flyout()
+ {
+ // TODO: Expose Placement
+ Placement = FlyoutPlacementMode.BottomEdgeAlignedLeft,
+ FlyoutPresenterStyle = FlyoutPresenterStyle,
+ Content = ColorPicker
+ };
+ }
+
+ if (CheckeredBackgroundBorder != null)
+ {
+ CheckeredBackgroundBorder.Loaded -= this.CheckeredBackgroundBorder_Loaded;
+ }
+
+ CheckeredBackgroundBorder = GetTemplateChild(nameof(CheckeredBackgroundBorder)) as Border;
+
+ if (CheckeredBackgroundBorder != null)
+ {
+ CheckeredBackgroundBorder.Loaded += this.CheckeredBackgroundBorder_Loaded;
+ }
+ }
+
+ private static void SelectedColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is ColorPickerButton instance && !(instance.ColorPicker is null))
+ {
+ instance.ColorPicker.Color = instance.SelectedColor;
+ }
+ }
+
+ private void ColorPicker_ColorChanged(Microsoft.UI.Xaml.Controls.ColorPicker sender, Microsoft.UI.Xaml.Controls.ColorChangedEventArgs args)
+ {
+ SelectedColor = args.NewColor;
+ }
+
+ private async void CheckeredBackgroundBorder_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is Border border)
+ {
+ await ColorPickerRenderingHelpers.UpdateBorderBackgroundWithCheckerAsync(border,
+ ColorPicker.CheckerBackgroundColor); // TODO: Check initialization
+ }
+ }
+}
diff --git a/components/ColorPicker/src/ColorPickerButton.xaml b/components/ColorPicker/src/ColorPickerButton.xaml
new file mode 100644
index 00000000..b9ce9048
--- /dev/null
+++ b/components/ColorPicker/src/ColorPickerButton.xaml
@@ -0,0 +1,219 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2
+ 3,3,6,3
+ 22
+ 32
+
+
+
+
+
+
diff --git a/components/ColorPicker/src/ColorPickerRenderingHelpers.cs b/components/ColorPicker/src/ColorPickerRenderingHelpers.cs
new file mode 100644
index 00000000..7ac31a24
--- /dev/null
+++ b/components/ColorPicker/src/ColorPickerRenderingHelpers.cs
@@ -0,0 +1,536 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Windows.UI;
+using Microsoft.UI;
+#if WINAPPSDK
+using Microsoft.UI.Xaml.Media.Imaging;
+using Colors = Microsoft.UI.Colors;
+#else
+using Windows.UI.Xaml.Media.Imaging;
+using Colors = Windows.UI.Colors;
+#endif
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// Contains the rendering methods used within .
+///
+internal class ColorPickerRenderingHelpers
+{
+ ///
+ /// Gets the default color used for checkered background squares (alternate squares are transparent).
+ /// Checkered backgrounds are used to help show transparency.
+ ///
+ internal static readonly Color CheckerBackgroundColor = Color.FromArgb(0x19, 0x80, 0x80, 0x80);
+
+ ///
+ /// Generates a new bitmap of the specified size by changing a specific color channel.
+ /// This will produce a gradient representing all possible differences of that color channel.
+ ///
+ /// The pixel width (X, horizontal) of the resulting bitmap.
+ /// The pixel height (Y, vertical) of the resulting bitmap.
+ /// The orientation of the resulting bitmap (gradient direction).
+ /// The color representation being used: RGBA or HSVA.
+ /// The specific color channel to vary.
+ /// The base HSV color used for channels not being changed.
+ /// The color of the checker background square.
+ /// Fix the alpha channel value to maximum during calculation.
+ /// This will remove any alpha/transparency from the other channel backgrounds.
+ /// Fix the saturation and value channels to maximum
+ /// during calculation in HSVA color representation.
+ /// This will ensure colors are always discernible regardless of saturation/value.
+ /// A new bitmap representing a gradient of color channel values.
+ public static async Task CreateChannelBitmapAsync(
+ int width,
+ int height,
+ Orientation orientation,
+ ColorRepresentation colorRepresentation,
+ ColorChannel channel,
+ HsvColor baseHsvColor,
+ Color? checkerColor,
+ bool isAlphaMaxForced,
+ bool isSaturationValueMaxForced)
+ {
+ if (width == 0 || height == 0)
+ {
+ return null!;
+ }
+
+ var bitmap = await Task.Run(async () =>
+ {
+ int pixelDataIndex = 0;
+ double channelStep;
+ byte[] bgraPixelData;
+ byte[]? bgraCheckeredPixelData = null;
+ Color baseRgbColor = Colors.White;
+ Color rgbColor;
+ int bgraPixelDataHeight;
+ int bgraPixelDataWidth;
+
+ // Allocate the buffer
+ // BGRA formatted color channels 1 byte each (4 bytes in a pixel)
+ bgraPixelData = new byte[width * height * 4];
+ bgraPixelDataHeight = height * 4;
+ bgraPixelDataWidth = width * 4;
+
+ // Maximize alpha channel value
+ if (isAlphaMaxForced &&
+ channel != ColorChannel.Alpha)
+ {
+ baseHsvColor = new HsvColor()
+ {
+ H = baseHsvColor.H,
+ S = baseHsvColor.S,
+ V = baseHsvColor.V,
+ A = 1.0
+ };
+ }
+
+ // Convert HSV to RGB once
+ if (colorRepresentation == ColorRepresentation.Rgba)
+ {
+ baseRgbColor = Helpers.ColorHelper.FromHsv(
+ baseHsvColor.H,
+ baseHsvColor.S,
+ baseHsvColor.V,
+ baseHsvColor.A);
+ }
+
+ // Maximize Saturation and Value channels when in HSVA mode
+ if (isSaturationValueMaxForced &&
+ colorRepresentation == ColorRepresentation.Hsva &&
+ channel != ColorChannel.Alpha)
+ {
+ switch (channel)
+ {
+ case ColorChannel.Channel1:
+ baseHsvColor = new HsvColor()
+ {
+ H = baseHsvColor.H,
+ S = 1.0,
+ V = 1.0,
+ A = baseHsvColor.A
+ };
+ break;
+ case ColorChannel.Channel2:
+ baseHsvColor = new HsvColor()
+ {
+ H = baseHsvColor.H,
+ S = baseHsvColor.S,
+ V = 1.0,
+ A = baseHsvColor.A
+ };
+ break;
+ case ColorChannel.Channel3:
+ baseHsvColor = new HsvColor()
+ {
+ H = baseHsvColor.H,
+ S = 1.0,
+ V = baseHsvColor.V,
+ A = baseHsvColor.A
+ };
+ break;
+ }
+ }
+
+ // Create a checkered background
+ if (checkerColor != null)
+ {
+ bgraCheckeredPixelData = await CreateCheckeredBitmapAsync(
+ width,
+ height,
+ checkerColor.Value);
+ }
+
+ // Create the color channel gradient
+ if (orientation == Orientation.Horizontal)
+ {
+ // Determine the numerical increment of the color steps within the channel
+ if (colorRepresentation == ColorRepresentation.Hsva)
+ {
+ if (channel == ColorChannel.Channel1)
+ {
+ channelStep = 360.0 / width;
+ }
+ else
+ {
+ channelStep = 1.0 / width;
+ }
+ }
+ else
+ {
+ channelStep = 255.0 / width;
+ }
+
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ if (y == 0)
+ {
+ rgbColor = GetColor(x * channelStep);
+
+ // Get a new color
+ bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(rgbColor.B * rgbColor.A / 255);
+ bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(rgbColor.G * rgbColor.A / 255);
+ bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(rgbColor.R * rgbColor.A / 255);
+ bgraPixelData[pixelDataIndex + 3] = rgbColor.A;
+ }
+ else
+ {
+ // Use the color in the row above
+ // Remember the pixel data is 1 dimensional instead of 2
+ bgraPixelData[pixelDataIndex + 0] = bgraPixelData[pixelDataIndex + 0 - bgraPixelDataWidth];
+ bgraPixelData[pixelDataIndex + 1] = bgraPixelData[pixelDataIndex + 1 - bgraPixelDataWidth];
+ bgraPixelData[pixelDataIndex + 2] = bgraPixelData[pixelDataIndex + 2 - bgraPixelDataWidth];
+ bgraPixelData[pixelDataIndex + 3] = bgraPixelData[pixelDataIndex + 3 - bgraPixelDataWidth];
+ }
+
+ pixelDataIndex += 4;
+ }
+ }
+ }
+ else
+ {
+ // Determine the numerical increment of the color steps within the channel
+ if (colorRepresentation == ColorRepresentation.Hsva)
+ {
+ if (channel == ColorChannel.Channel1)
+ {
+ channelStep = 360.0 / height;
+ }
+ else
+ {
+ channelStep = 1.0 / height;
+ }
+ }
+ else
+ {
+ channelStep = 255.0 / height;
+ }
+
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ if (x == 0)
+ {
+ // The lowest channel value should be at the 'bottom' of the bitmap
+ rgbColor = GetColor((height - 1 - y) * channelStep);
+
+ // Get a new color
+ bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(rgbColor.B * rgbColor.A / 255);
+ bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(rgbColor.G * rgbColor.A / 255);
+ bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(rgbColor.R * rgbColor.A / 255);
+ bgraPixelData[pixelDataIndex + 3] = rgbColor.A;
+ }
+ else
+ {
+ // Use the color in the column to the left
+ // Remember the pixel data is 1 dimensional instead of 2
+ bgraPixelData[pixelDataIndex + 0] = bgraPixelData[pixelDataIndex - 4];
+ bgraPixelData[pixelDataIndex + 1] = bgraPixelData[pixelDataIndex - 3];
+ bgraPixelData[pixelDataIndex + 2] = bgraPixelData[pixelDataIndex - 2];
+ bgraPixelData[pixelDataIndex + 3] = bgraPixelData[pixelDataIndex - 1];
+ }
+
+ pixelDataIndex += 4;
+ }
+ }
+ }
+
+ // Composite the checkered background with color channel gradient for final result
+ // The height/width are not checked as both bitmaps were built with the same values
+ if ((checkerColor != null) &&
+ (bgraCheckeredPixelData != null))
+ {
+ pixelDataIndex = 0;
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ /* The following algorithm is used to blend the two bitmaps creating the final composite.
+ * In this formula, pixel data is normalized 0..1, actual pixel data is in the range 0..255.
+ * The color channel gradient should apply OVER the checkered background.
+ *
+ * R = R0 * A0 * (1 - A1) + R1 * A1 = RA0 * (1 - A1) + RA1
+ * G = G0 * A0 * (1 - A1) + G1 * A1 = GA0 * (1 - A1) + GA1
+ * B = B0 * A0 * (1 - A1) + B1 * A1 = BA0 * (1 - A1) + BA1
+ * A = A0 * (1 - A1) + A1 = A0 * (1 - A1) + A1
+ *
+ * Considering only the red channel, some algebraic transformation is applied to
+ * make the math quicker to solve.
+ *
+ * => ((RA0 / 255.0) * (1.0 - A1 / 255.0) + (RA1 / 255.0)) * 255.0
+ * => ((RA0 * 255) - (RA0 * A1) + (RA1 * 255)) / 255
+ */
+
+ // Bottom layer
+ byte rXa0 = bgraCheckeredPixelData[pixelDataIndex + 2];
+ byte gXa0 = bgraCheckeredPixelData[pixelDataIndex + 1];
+ byte bXa0 = bgraCheckeredPixelData[pixelDataIndex + 0];
+ byte a0 = bgraCheckeredPixelData[pixelDataIndex + 3];
+
+ // Top layer
+ byte rXa1 = bgraPixelData[pixelDataIndex + 2];
+ byte gXa1 = bgraPixelData[pixelDataIndex + 1];
+ byte bXa1 = bgraPixelData[pixelDataIndex + 0];
+ byte a1 = bgraPixelData[pixelDataIndex + 3];
+
+ bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(((bXa0 * 255) - (bXa0 * a1) + (bXa1 * 255)) / 255);
+ bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(((gXa0 * 255) - (gXa0 * a1) + (gXa1 * 255)) / 255);
+ bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(((rXa0 * 255) - (rXa0 * a1) + (rXa1 * 255)) / 255);
+ bgraPixelData[pixelDataIndex + 3] = Convert.ToByte(((a0 * 255) - (a0 * a1) + (a1 * 255)) / 255);
+
+ pixelDataIndex += 4;
+ }
+ }
+ }
+
+ Color GetColor(double channelValue)
+ {
+ Color newRgbColor = Colors.White;
+
+ switch (channel)
+ {
+ case ColorChannel.Channel1:
+ {
+ if (colorRepresentation == ColorRepresentation.Hsva)
+ {
+ // Sweep hue
+ newRgbColor = Helpers.ColorHelper.FromHsv(
+ Math.Clamp(channelValue, 0.0, 360.0),
+ baseHsvColor.S,
+ baseHsvColor.V,
+ baseHsvColor.A);
+ }
+ else
+ {
+ // Sweep red
+ newRgbColor = new Color
+ {
+ R = Convert.ToByte(Math.Clamp(channelValue, 0.0, 255.0)),
+ G = baseRgbColor.G,
+ B = baseRgbColor.B,
+ A = baseRgbColor.A
+ };
+ }
+
+ break;
+ }
+
+ case ColorChannel.Channel2:
+ {
+ if (colorRepresentation == ColorRepresentation.Hsva)
+ {
+ // Sweep saturation
+ newRgbColor = Helpers.ColorHelper.FromHsv(
+ baseHsvColor.H,
+ Math.Clamp(channelValue, 0.0, 1.0),
+ baseHsvColor.V,
+ baseHsvColor.A);
+ }
+ else
+ {
+ // Sweep green
+ newRgbColor = new Color
+ {
+ R = baseRgbColor.R,
+ G = Convert.ToByte(Math.Clamp(channelValue, 0.0, 255.0)),
+ B = baseRgbColor.B,
+ A = baseRgbColor.A
+ };
+ }
+
+ break;
+ }
+
+ case ColorChannel.Channel3:
+ {
+ if (colorRepresentation == ColorRepresentation.Hsva)
+ {
+ // Sweep value
+ newRgbColor = Helpers.ColorHelper.FromHsv(
+ baseHsvColor.H,
+ baseHsvColor.S,
+ Math.Clamp(channelValue, 0.0, 1.0),
+ baseHsvColor.A);
+ }
+ else
+ {
+ // Sweep blue
+ newRgbColor = new Color
+ {
+ R = baseRgbColor.R,
+ G = baseRgbColor.G,
+ B = Convert.ToByte(Math.Clamp(channelValue, 0.0, 255.0)),
+ A = baseRgbColor.A
+ };
+ }
+
+ break;
+ }
+
+ case ColorChannel.Alpha:
+ {
+ if (colorRepresentation == ColorRepresentation.Hsva)
+ {
+ // Sweep alpha
+ newRgbColor = Helpers.ColorHelper.FromHsv(
+ baseHsvColor.H,
+ baseHsvColor.S,
+ baseHsvColor.V,
+ Math.Clamp(channelValue, 0.0, 1.0));
+ }
+ else
+ {
+ // Sweep alpha
+ newRgbColor = new Color
+ {
+ R = baseRgbColor.R,
+ G = baseRgbColor.G,
+ B = baseRgbColor.B,
+ A = Convert.ToByte(Math.Clamp(channelValue, 0.0, 255.0))
+ };
+ }
+
+ break;
+ }
+ }
+
+ return newRgbColor;
+ }
+
+ return bgraPixelData;
+ });
+
+ return bitmap;
+ }
+
+ ///
+ /// Generates a new checkered bitmap of the specified size.
+ ///
+ ///
+ /// This is a port and heavy modification of the code here:
+ /// https://github.com/microsoft/microsoft-ui-xaml/blob/865e4fcc00e8649baeaec1ba7daeca398671aa72/dev/ColorPicker/ColorHelpers.cpp#L363
+ /// UWP needs TiledBrush support.
+ ///
+ /// The pixel width (X, horizontal) of the checkered bitmap.
+ /// The pixel height (Y, vertical) of the checkered bitmap.
+ /// The color of the checker square.
+ /// A new checkered bitmap of the specified size.
+ public static async Task CreateCheckeredBitmapAsync(
+ int width,
+ int height,
+ Color checkerColor)
+ {
+ // The size of the checker is important. You want it big enough that the grid is clearly discernible.
+ // However, the squares should be small enough they don't appear unnaturally cut at the edge of backgrounds.
+ int checkerSize = 4;
+
+ if (width == 0 || height == 0)
+ {
+ return null!;
+ }
+
+ var bitmap = await Task.Run(() =>
+ {
+ int pixelDataIndex = 0;
+ byte[] bgraPixelData;
+
+ // Allocate the buffer
+ // BGRA formatted color channels 1 byte each (4 bytes in a pixel)
+ bgraPixelData = new byte[width * height * 4];
+
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ // We want the checkered pattern to alternate both vertically and horizontally.
+ // In order to achieve that, we'll toggle visibility of the current pixel on or off
+ // depending on both its x- and its y-position. If x == CheckerSize, we'll turn visibility off,
+ // but then if y == CheckerSize, we'll turn it back on.
+ // The below is a shorthand for the above intent.
+ bool pixelShouldBeBlank = ((x / checkerSize) + (y / checkerSize)) % 2 == 0 ? true : false;
+
+ // Remember, use BGRA pixel format with pre-multiplied alpha values
+ if (pixelShouldBeBlank)
+ {
+ bgraPixelData[pixelDataIndex + 0] = 0;
+ bgraPixelData[pixelDataIndex + 1] = 0;
+ bgraPixelData[pixelDataIndex + 2] = 0;
+ bgraPixelData[pixelDataIndex + 3] = 0;
+ }
+ else
+ {
+ bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(checkerColor.B * checkerColor.A / 255);
+ bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(checkerColor.G * checkerColor.A / 255);
+ bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(checkerColor.R * checkerColor.A / 255);
+ bgraPixelData[pixelDataIndex + 3] = checkerColor.A;
+ }
+
+ pixelDataIndex += 4;
+ }
+ }
+
+ return bgraPixelData;
+ });
+
+ return bitmap;
+ }
+
+ ///
+ /// Converts the given bitmap (in raw BGRA pre-multiplied alpha pixels) into an image brush
+ /// that can be used in the UI.
+ ///
+ /// The bitmap (in raw BGRA pre-multiplied alpha pixels) to convert to a brush.
+ /// The pixel width of the bitmap.
+ /// The pixel height of the bitmap.
+ /// A new ImageBrush.
+ public static async Task BitmapToBrushAsync(
+ byte[] bitmap,
+ int width,
+ int height)
+ {
+ var writableBitmap = new WriteableBitmap(width, height);
+ using (Stream stream = writableBitmap.PixelBuffer.AsStream())
+ {
+ await stream.WriteAsync(bitmap, 0, bitmap.Length);
+ }
+
+ var brush = new ImageBrush()
+ {
+ ImageSource = writableBitmap,
+ Stretch = Stretch.None
+ };
+
+ return brush;
+ }
+
+ ///
+ /// Centralizes code to create a checker brush for a .
+ ///
+ /// Border which will have its Background modified.
+ /// Color to use for transparent checkerboard.
+ /// Task
+ public static async Task UpdateBorderBackgroundWithCheckerAsync(Border border, Color color)
+ {
+ if (border != null)
+ {
+ int width = Convert.ToInt32(border.ActualWidth);
+ int height = Convert.ToInt32(border.ActualHeight);
+
+ var bitmap = await ColorPickerRenderingHelpers.CreateCheckeredBitmapAsync(
+ width,
+ height,
+ color);
+
+ if (bitmap != null)
+ {
+ border.Background = await ColorPickerRenderingHelpers.BitmapToBrushAsync(bitmap, width, height);
+ }
+ }
+ }
+}
diff --git a/components/ColorPicker/src/ColorPickerSlider.Properties.cs b/components/ColorPicker/src/ColorPickerSlider.Properties.cs
new file mode 100644
index 00000000..9efca1bc
--- /dev/null
+++ b/components/ColorPicker/src/ColorPickerSlider.Properties.cs
@@ -0,0 +1,243 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation 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;
+
+#if WINAPPSDK
+using Colors = Microsoft.UI.Colors;
+#else
+using Colors = Windows.UI.Colors;
+#endif
+
+namespace CommunityToolkit.WinUI.Controls.Primitives;
+
+///
+public partial class ColorPickerSlider : Slider
+{
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ColorProperty =
+ DependencyProperty.Register(
+ nameof(Color),
+ typeof(Color),
+ typeof(ColorPickerSlider),
+ new PropertyMetadata(
+ Colors.White,
+ (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e)));
+
+ ///
+ /// Gets or sets the RGB color represented by the slider.
+ /// For accuracy use instead.
+ ///
+ public Color Color
+ {
+ get => (Color)this.GetValue(ColorProperty);
+ set
+ {
+ if (object.Equals(value, this.GetValue(ColorProperty)) == false)
+ {
+ this.SetValue(ColorProperty, value);
+ }
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ColorChannelProperty =
+ DependencyProperty.Register(
+ nameof(ColorChannel),
+ typeof(ColorChannel),
+ typeof(ColorPickerSlider),
+ new PropertyMetadata(
+ ColorChannel.Channel1,
+ (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e)));
+
+ ///
+ /// Gets or sets the color channel represented by the slider.
+ ///
+ public ColorChannel ColorChannel
+ {
+ get => (ColorChannel)this.GetValue(ColorChannelProperty);
+ set
+ {
+ if (object.Equals(value, this.GetValue(ColorChannelProperty)) == false)
+ {
+ this.SetValue(ColorChannelProperty, value);
+ }
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ColorRepresentationProperty =
+ DependencyProperty.Register(
+ nameof(ColorRepresentation),
+ typeof(ColorRepresentation),
+ typeof(ColorPickerSlider),
+ new PropertyMetadata(
+ ColorRepresentation.Rgba,
+ (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e)));
+
+ ///
+ /// Gets or sets the color representation used by the slider.
+ ///
+ public ColorRepresentation ColorRepresentation
+ {
+ get => (ColorRepresentation)this.GetValue(ColorRepresentationProperty);
+ set
+ {
+ if (object.Equals(value, this.GetValue(ColorRepresentationProperty)) == false)
+ {
+ this.SetValue(ColorRepresentationProperty, value);
+ }
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty DefaultForegroundProperty =
+ DependencyProperty.Register(
+ nameof(DefaultForeground),
+ typeof(Brush),
+ typeof(ColorPickerSlider),
+ new PropertyMetadata(
+ null,
+ (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e)));
+
+ ///
+ /// Gets or sets the default foreground brush to use when the slider background is hardly visible and nearly transparent.
+ /// Generally, this should be the default Foreground text brush.
+ ///
+ public Brush DefaultForeground
+ {
+ get => (Brush)this.GetValue(DefaultForegroundProperty);
+ set
+ {
+ if (object.Equals(value, this.GetValue(DefaultForegroundProperty)) == false)
+ {
+ this.SetValue(DefaultForegroundProperty, value);
+ }
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty HsvColorProperty =
+ DependencyProperty.Register(
+ nameof(HsvColor),
+ typeof(HsvColor),
+ typeof(ColorPickerSlider),
+ new PropertyMetadata(
+ Colors.White.ToHsv(),
+ (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e)));
+
+ ///
+ /// Gets or sets the HSV color represented by the slider.
+ /// This is the preferred color property for accuracy.
+ ///
+ public HsvColor HsvColor
+ {
+ get => (HsvColor)this.GetValue(HsvColorProperty);
+ set
+ {
+ if (object.Equals(value, this.GetValue(HsvColorProperty)) == false)
+ {
+ this.SetValue(HsvColorProperty, value);
+ }
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty IsAlphaMaxForcedProperty =
+ DependencyProperty.Register(
+ nameof(IsAlphaMaxForced),
+ typeof(bool),
+ typeof(ColorPickerSlider),
+ new PropertyMetadata(
+ true,
+ (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e)));
+
+ ///
+ /// Gets or sets a value indicating whether the alpha channel is always forced to maximum for channels
+ /// other than .
+ /// This ensures that the background is always visible and never transparent regardless of the actual color.
+ ///
+ public bool IsAlphaMaxForced
+ {
+ get => (bool)this.GetValue(IsAlphaMaxForcedProperty);
+ set
+ {
+ if (object.Equals(value, this.GetValue(IsAlphaMaxForcedProperty)) == false)
+ {
+ this.SetValue(IsAlphaMaxForcedProperty, value);
+ }
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty IsAutoUpdatingEnabledProperty =
+ DependencyProperty.Register(
+ nameof(IsAutoUpdatingEnabled),
+ typeof(bool),
+ typeof(ColorPickerSlider),
+ new PropertyMetadata(
+ true,
+ (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e)));
+
+ ///
+ /// Gets or sets a value indicating whether automatic background and foreground updates will be
+ /// calculated when the set color changes. This can be disabled for performance reasons when working with
+ /// multiple sliders.
+ ///
+ public bool IsAutoUpdatingEnabled
+ {
+ get => (bool)this.GetValue(IsAutoUpdatingEnabledProperty);
+ set
+ {
+ if (object.Equals(value, this.GetValue(IsAutoUpdatingEnabledProperty)) == false)
+ {
+ this.SetValue(IsAutoUpdatingEnabledProperty, value);
+ }
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty IsSaturationValueMaxForcedProperty =
+ DependencyProperty.Register(
+ nameof(IsSaturationValueMaxForced),
+ typeof(bool),
+ typeof(ColorPickerSlider),
+ new PropertyMetadata(
+ true,
+ (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e)));
+
+ ///
+ /// Gets or sets a value indicating whether the saturation and value channels are always forced to maximum values
+ /// when in HSVA color representation. Only channel values other than will be changed.
+ /// This ensures, for example, that the Hue background is always visible and never washed out regardless of the actual color.
+ ///
+ public bool IsSaturationValueMaxForced
+ {
+ get => (bool)this.GetValue(IsSaturationValueMaxForcedProperty);
+ set
+ {
+ if (object.Equals(value, this.GetValue(IsSaturationValueMaxForcedProperty)) == false)
+ {
+ this.SetValue(IsSaturationValueMaxForcedProperty, value);
+ }
+ }
+ }
+}
diff --git a/components/ColorPicker/src/ColorPickerSlider.cs b/components/ColorPicker/src/ColorPickerSlider.cs
new file mode 100644
index 00000000..1bff1d26
--- /dev/null
+++ b/components/ColorPicker/src/ColorPickerSlider.cs
@@ -0,0 +1,293 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation 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 CommunityToolkit.WinUI.Controls.Primitives;
+
+///
+/// A slider that represents a single color channel for use in the .
+///
+public partial class ColorPickerSlider : Slider
+{
+ // TODO Combine this with the ColorPicker field or make a property
+ internal Color CheckerBackgroundColor { get; set; } = Color.FromArgb(0x19, 0x80, 0x80, 0x80); // Overridden later
+
+ private Size oldSize = Size.Empty;
+ private Size measuredSize = Size.Empty;
+ private Size cachedSize = Size.Empty;
+
+ /***************************************************************************************
+ *
+ * Constructor/Destructor
+ *
+ ***************************************************************************************/
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ColorPickerSlider()
+ : base()
+ {
+ this.DefaultStyleKey = typeof(ColorPickerSlider);
+ }
+
+ /***************************************************************************************
+ *
+ * Methods
+ *
+ ***************************************************************************************/
+
+ ///
+ /// Update the slider's Foreground and Background brushes based on the current slider state and color.
+ ///
+ ///
+ /// Manually refreshes the background gradient of the slider.
+ /// This is callable separately for performance reasons.
+ ///
+ public void UpdateColors()
+ {
+ HsvColor hsvColor = this.HsvColor;
+
+ // Calculate and set the background
+ this.UpdateBackground(hsvColor);
+
+ // Calculate and set the foreground ensuring contrast with the background
+ Color rgbColor = Helpers.ColorHelper.FromHsv(hsvColor.H, hsvColor.S, hsvColor.V, hsvColor.A);
+ Color selectedRgbColor;
+ double sliderPercent = this.Value / (this.Maximum - this.Minimum);
+
+ if (this.ColorRepresentation == ColorRepresentation.Hsva)
+ {
+ if (this.IsAlphaMaxForced &&
+ this.ColorChannel != ColorChannel.Alpha)
+ {
+ hsvColor = new HsvColor()
+ {
+ H = hsvColor.H,
+ S = hsvColor.S,
+ V = hsvColor.V,
+ A = 1.0
+ };
+ }
+
+ switch (this.ColorChannel)
+ {
+ case ColorChannel.Channel1:
+ {
+ var channelValue = Math.Clamp(sliderPercent * 360.0, 0.0, 360.0);
+
+ hsvColor = new HsvColor()
+ {
+ H = channelValue,
+ S = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.S,
+ V = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.V,
+ A = hsvColor.A
+ };
+ break;
+ }
+
+ case ColorChannel.Channel2:
+ {
+ var channelValue = Math.Clamp(sliderPercent * 1.0, 0.0, 1.0);
+
+ hsvColor = new HsvColor()
+ {
+ H = hsvColor.H,
+ S = channelValue,
+ V = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.V,
+ A = hsvColor.A
+ };
+ break;
+ }
+
+ case ColorChannel.Channel3:
+ {
+ var channelValue = Math.Clamp(sliderPercent * 1.0, 0.0, 1.0);
+
+ hsvColor = new HsvColor()
+ {
+ H = hsvColor.H,
+ S = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.S,
+ V = channelValue,
+ A = hsvColor.A
+ };
+ break;
+ }
+ }
+
+ selectedRgbColor = Helpers.ColorHelper.FromHsv(
+ hsvColor.H,
+ hsvColor.S,
+ hsvColor.V,
+ hsvColor.A);
+ }
+ else
+ {
+ if (this.IsAlphaMaxForced &&
+ this.ColorChannel != ColorChannel.Alpha)
+ {
+ rgbColor = new Color()
+ {
+ R = rgbColor.R,
+ G = rgbColor.G,
+ B = rgbColor.B,
+ A = 255
+ };
+ }
+
+ byte channelValue = Convert.ToByte(Math.Clamp(sliderPercent * 255.0, 0.0, 255.0));
+
+ switch (this.ColorChannel)
+ {
+ case ColorChannel.Channel1:
+ rgbColor = new Color()
+ {
+ R = channelValue,
+ G = rgbColor.G,
+ B = rgbColor.B,
+ A = rgbColor.A
+ };
+ break;
+ case ColorChannel.Channel2:
+ rgbColor = new Color()
+ {
+ R = rgbColor.R,
+ G = channelValue,
+ B = rgbColor.B,
+ A = rgbColor.A
+ };
+ break;
+ case ColorChannel.Channel3:
+ rgbColor = new Color()
+ {
+ R = rgbColor.R,
+ G = rgbColor.G,
+ B = channelValue,
+ A = rgbColor.A
+ };
+ break;
+ }
+
+ selectedRgbColor = rgbColor;
+ }
+
+ var converter = new ContrastBrushConverter();
+
+ return;
+ }
+
+ ///
+ /// Generates a new background image for the color channel slider and applies it.
+ ///
+ private async void UpdateBackground(HsvColor color)
+ {
+ /* Updates may be requested when sliders are not in the visual tree.
+ * For first-time load this is handled by the Loaded event.
+ * However, after that problems may arise, consider the following case:
+ *
+ * (1) Backgrounds are drawn normally the first time on Loaded.
+ * Actual height/width are available.
+ * (2) The palette tab is selected which has no sliders
+ * (3) The picker flyout is closed
+ * (4) Externally the color is changed
+ * The color change will trigger slider background updates but
+ * with the flyout closed, actual height/width are zero.
+ * No zero size bitmap can be generated.
+ * (5) The picker flyout is re-opened by the user and the default
+ * last-opened tab will be viewed: palette.
+ * No loaded events will be fired for sliders. The color change
+ * event was already handled in (4). The sliders will never
+ * be updated.
+ *
+ * In this case the sliders become out of sync with the Color because there is no way
+ * to tell when they actually come into view. To work around this, force a re-render of
+ * the background with the last size of the slider. This last size will be when it was
+ * last loaded or updated.
+ *
+ * In the future additional consideration may be required for SizeChanged of the control.
+ * This work-around will also cause issues if display scaling changes in the special
+ * case where cached sizes are required.
+ */
+ var width = Convert.ToInt32(this.ActualWidth);
+ var height = Convert.ToInt32(this.ActualHeight);
+
+ if (width == 0 || height == 0)
+ {
+ // Attempt to use the last size if it was available
+ if (this.cachedSize.IsEmpty == false)
+ {
+ width = Convert.ToInt32(this.cachedSize.Width);
+ height = Convert.ToInt32(this.cachedSize.Height);
+ }
+ }
+ else
+ {
+ this.cachedSize = new Size(width, height);
+ }
+
+ var bitmap = await ColorPickerRenderingHelpers.CreateChannelBitmapAsync(
+ width,
+ height,
+ this.Orientation,
+ this.ColorRepresentation,
+ this.ColorChannel,
+ color,
+ this.CheckerBackgroundColor,
+ this.IsAlphaMaxForced,
+ this.IsSaturationValueMaxForced);
+
+ if (bitmap != null)
+ {
+ this.Background = await ColorPickerRenderingHelpers.BitmapToBrushAsync(bitmap, width, height);
+ }
+
+ return;
+ }
+
+ ///
+ /// Measures the size in layout required for child elements and determines a size for the
+ /// FrameworkElement-derived class.
+ ///
+ ///
+ ///
+ /// Slider has some critical bugs:
+ ///
+ /// * https://github.com/microsoft/microsoft-ui-xaml/issues/477
+ /// * https://social.msdn.microsoft.com/Forums/sqlserver/en-US/0d3a2e64-d192-4250-b583-508a02bd75e1/uwp-bug-crash-layoutcycleexception-because-of-slider-under-certain-circumstances?forum=wpdevelop
+ ///
+ ///
+ /// The available size that this element can give to child elements.
+ /// Infinity can be specified as a value to indicate that the element will size to whatever content
+ /// is available.
+ /// The size that this element determines it needs during layout,
+ /// based on its calculations of child element sizes.
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ if (!Size.Equals(oldSize, availableSize))
+ {
+ measuredSize = base.MeasureOverride(availableSize);
+ oldSize = availableSize;
+ }
+
+ return measuredSize;
+ }
+
+ private void OnDependencyPropertyChanged(object sender, DependencyPropertyChangedEventArgs args)
+ {
+ if (object.ReferenceEquals(args.Property, ColorProperty))
+ {
+ // Sync with HSV (which is primary)
+ this.HsvColor = this.Color.ToHsv();
+ }
+
+ if (this.IsAutoUpdatingEnabled)
+ {
+ this.UpdateColors();
+ }
+
+ return;
+ }
+}
diff --git a/components/ColorPicker/src/ColorPickerSlider.xaml b/components/ColorPicker/src/ColorPickerSlider.xaml
new file mode 100644
index 00000000..1e074703
--- /dev/null
+++ b/components/ColorPicker/src/ColorPickerSlider.xaml
@@ -0,0 +1,259 @@
+
+
+
+
+
+
+
+
+
diff --git a/components/ColorPicker/src/ColorPreviewer.Properties.cs b/components/ColorPicker/src/ColorPreviewer.Properties.cs
new file mode 100644
index 00000000..eb6bda8d
--- /dev/null
+++ b/components/ColorPicker/src/ColorPreviewer.Properties.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI.Helpers;
+
+#if WINAPPSDK
+using Colors = Microsoft.UI.Colors;
+#else
+using Colors = Windows.UI.Colors;
+#endif
+
+namespace CommunityToolkit.WinUI.Controls.Primitives;
+
+///
+public partial class ColorPreviewer
+{
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty HsvColorProperty =
+ DependencyProperty.Register(
+ nameof(HsvColor),
+ typeof(HsvColor),
+ typeof(ColorPreviewer),
+ new PropertyMetadata(
+ Colors.Transparent.ToHsv(),
+ (s, e) => (s as ColorPreviewer)?.OnDependencyPropertyChanged(s, e)));
+
+ ///
+ /// Gets or sets the HSV color represented by the color previewer.
+ /// This is the preferred color property for accuracy.
+ ///
+ public HsvColor HsvColor
+ {
+ get => (HsvColor)this.GetValue(HsvColorProperty);
+ set
+ {
+ if (object.Equals(value, this.GetValue(HsvColorProperty)) == false)
+ {
+ this.SetValue(HsvColorProperty, value);
+ }
+ }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ShowAccentColorsProperty =
+ DependencyProperty.Register(
+ nameof(ShowAccentColors),
+ typeof(bool),
+ typeof(ColorPreviewer),
+ new PropertyMetadata(
+ true,
+ (s, e) => (s as ColorPreviewer)?.OnDependencyPropertyChanged(s, e)));
+
+ ///
+ /// Gets or sets a value indicating whether accent colors are shown along
+ /// with the preview color.
+ ///
+ public bool ShowAccentColors
+ {
+ get => (bool)this.GetValue(ShowAccentColorsProperty);
+ set
+ {
+ if (object.Equals(value, this.GetValue(ShowAccentColorsProperty)) == false)
+ {
+ this.SetValue(ShowAccentColorsProperty, value);
+ }
+ }
+ }
+}
diff --git a/components/ColorPicker/src/ColorPreviewer.cs b/components/ColorPicker/src/ColorPreviewer.cs
new file mode 100644
index 00000000..f53b2933
--- /dev/null
+++ b/components/ColorPicker/src/ColorPreviewer.cs
@@ -0,0 +1,197 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Globalization;
+
+namespace CommunityToolkit.WinUI.Controls.Primitives;
+
+///
+/// Presents a 's preview color with optional accent colors.
+///
+[TemplatePart(Name = nameof(ColorPreviewer.CheckeredBackgroundBorder), Type = typeof(Border))]
+[TemplatePart(Name = nameof(ColorPreviewer.P1PreviewBorder), Type = typeof(Border))]
+[TemplatePart(Name = nameof(ColorPreviewer.P2PreviewBorder), Type = typeof(Border))]
+[TemplatePart(Name = nameof(ColorPreviewer.N1PreviewBorder), Type = typeof(Border))]
+[TemplatePart(Name = nameof(ColorPreviewer.N2PreviewBorder), Type = typeof(Border))]
+[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1501:Statement should not be on a single line", Justification = "Inline brackets are used to improve code readability with repeated null checks.")]
+[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "Whitespace is used to align code in columns for readability.")]
+[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Only template parts start with a capital letter. This differentiates them from other fields.")]
+public partial class ColorPreviewer : Control
+{
+ ///
+ /// Event for when a color change is requested by user interaction with the previewer.
+ ///
+ public event EventHandler ColorChangeRequested;
+
+ private bool eventsConnected = false;
+
+ private Border CheckeredBackgroundBorder;
+
+ private Border N1PreviewBorder;
+ private Border N2PreviewBorder;
+ private Border P1PreviewBorder;
+ private Border P2PreviewBorder;
+
+ /***************************************************************************************
+ *
+ * Constructor/Destructor
+ *
+ ***************************************************************************************/
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
+ public ColorPreviewer()
+#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
+ : base()
+ {
+ this.DefaultStyleKey = typeof(ColorPreviewer);
+ }
+
+ /***************************************************************************************
+ *
+ * Methods
+ *
+ ***************************************************************************************/
+
+ ///
+ /// Connects or disconnects all control event handlers.
+ ///
+ /// True to connect event handlers, otherwise false.
+ private void ConnectEvents(bool connected)
+ {
+ if (connected == true && this.eventsConnected == false)
+ {
+ // Add all events
+ if (this.CheckeredBackgroundBorder != null) { this.CheckeredBackgroundBorder.Loaded += CheckeredBackgroundBorder_Loaded; }
+
+ if (this.N1PreviewBorder != null) { this.N1PreviewBorder.PointerPressed += PreviewBorder_PointerPressed; }
+ if (this.N2PreviewBorder != null) { this.N2PreviewBorder.PointerPressed += PreviewBorder_PointerPressed; }
+ if (this.P1PreviewBorder != null) { this.P1PreviewBorder.PointerPressed += PreviewBorder_PointerPressed; }
+ if (this.P2PreviewBorder != null) { this.P2PreviewBorder.PointerPressed += PreviewBorder_PointerPressed; }
+
+ this.eventsConnected = true;
+ }
+ else if (connected == false && this.eventsConnected == true)
+ {
+ // Remove all events
+ if (this.CheckeredBackgroundBorder != null) { this.CheckeredBackgroundBorder.Loaded -= CheckeredBackgroundBorder_Loaded; }
+
+ if (this.N1PreviewBorder != null) { this.N1PreviewBorder.PointerPressed -= PreviewBorder_PointerPressed; }
+ if (this.N2PreviewBorder != null) { this.N2PreviewBorder.PointerPressed -= PreviewBorder_PointerPressed; }
+ if (this.P1PreviewBorder != null) { this.P1PreviewBorder.PointerPressed -= PreviewBorder_PointerPressed; }
+ if (this.P2PreviewBorder != null) { this.P2PreviewBorder.PointerPressed -= PreviewBorder_PointerPressed; }
+
+ this.eventsConnected = false;
+ }
+
+ return;
+ }
+
+ /***************************************************************************************
+ *
+ * OnEvent Overridable Methods
+ *
+ ***************************************************************************************/
+
+ ///
+ protected override void OnApplyTemplate()
+ {
+ // Remove any existing events present if the control was previously loaded then unloaded
+ this.ConnectEvents(false);
+
+ this.CheckeredBackgroundBorder = (Border)this.GetTemplateChild(nameof(CheckeredBackgroundBorder));
+
+ this.N1PreviewBorder = (Border)this.GetTemplateChild(nameof(N1PreviewBorder));
+ this.N2PreviewBorder = (Border)this.GetTemplateChild(nameof(N2PreviewBorder));
+ this.P1PreviewBorder = (Border)this.GetTemplateChild(nameof(P1PreviewBorder));
+ this.P2PreviewBorder = (Border)this.GetTemplateChild(nameof(P2PreviewBorder));
+
+ // Must connect after controls are resolved
+ this.ConnectEvents(true);
+
+ base.OnApplyTemplate();
+ }
+
+ ///
+ /// Method called whenever a dependency property value is changed.
+ /// This may happen through binding directly or a property setter.
+ ///
+ /// The sender of the event.
+ /// The event arguments.
+ protected virtual void OnDependencyPropertyChanged(object sender, DependencyPropertyChangedEventArgs args)
+ {
+ VisualStateManager.GoToState(this, ShowAccentColors ? "AccentColorsVisible" : "AccentColorsCollapsed", true);
+ return;
+ }
+
+ ///
+ /// Called before the event occurs.
+ ///
+ /// The newly requested color.
+ protected virtual void OnColorChangeRequested(HsvColor color)
+ {
+ this.ColorChangeRequested?.Invoke(this, color);
+ return;
+ }
+
+ /***************************************************************************************
+ *
+ * Event Handling
+ *
+ ***************************************************************************************/
+
+ ///
+ /// Event handler to draw checkered backgrounds on-demand as controls are loaded.
+ ///
+ private async void CheckeredBackgroundBorder_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is Border border)
+ {
+ int width = Convert.ToInt32(border.ActualWidth);
+ int height = Convert.ToInt32(border.ActualHeight);
+
+ var bitmap = await ColorPickerRenderingHelpers.CreateCheckeredBitmapAsync(
+ width,
+ height,
+ ColorPickerRenderingHelpers.CheckerBackgroundColor);
+
+ if (bitmap != null)
+ {
+ border.Background = await ColorPickerRenderingHelpers.BitmapToBrushAsync(bitmap, width, height);
+ }
+ }
+
+ return;
+ }
+
+ ///
+ /// Event handler for when a preview color panel is pressed.
+ /// This will update the color to the background of the pressed panel.
+ ///
+ private void PreviewBorder_PointerPressed(object sender, PointerRoutedEventArgs e)
+ {
+ if (sender is Border border)
+ {
+ int accentStep = 0;
+ HsvColor hsvColor = this.HsvColor;
+
+ // Get the value component delta
+ try
+ {
+ if (border.Tag?.ToString() is string tag)
+ {
+ accentStep = int.Parse(tag, CultureInfo.InvariantCulture);
+ }
+ }
+ catch { }
+
+ HsvColor newHsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep);
+ this.OnColorChangeRequested(newHsvColor);
+
+ return;
+ }
+ }
+}
diff --git a/components/ColorPicker/src/ColorPreviewer.xaml b/components/ColorPicker/src/ColorPreviewer.xaml
new file mode 100644
index 00000000..1b7ab1b9
--- /dev/null
+++ b/components/ColorPicker/src/ColorPreviewer.xaml
@@ -0,0 +1,113 @@
+
+
+
+
+
diff --git a/components/ColorPicker/src/ColorRepresentation.cs b/components/ColorPicker/src/ColorRepresentation.cs
new file mode 100644
index 00000000..d94c47a9
--- /dev/null
+++ b/components/ColorPicker/src/ColorRepresentation.cs
@@ -0,0 +1,22 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// Defines how colors are represented.
+///
+[EditorBrowsable(EditorBrowsableState.Advanced)]
+public enum ColorRepresentation
+{
+ ///
+ /// Color is represented by hue, saturation, value and alpha channels.
+ ///
+ Hsva,
+
+ ///
+ /// Color is represented by red, green, blue and alpha channels.
+ ///
+ Rgba
+}
diff --git a/components/ColorPicker/src/CommunityToolkit.WinUI.Controls.ColorPicker.csproj b/components/ColorPicker/src/CommunityToolkit.WinUI.Controls.ColorPicker.csproj
new file mode 100644
index 00000000..846b808c
--- /dev/null
+++ b/components/ColorPicker/src/CommunityToolkit.WinUI.Controls.ColorPicker.csproj
@@ -0,0 +1,32 @@
+
+
+
+ ColorPicker
+ This package contains ColorPicker.
+
+
+ CommunityToolkit.WinUI.Controls.ColorPickerRns
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(PackageIdPrefix).$(PackageIdVariant).Controls.$(ToolkitComponentName)
+
+
+
+
+ <_LayoutFile Remove="@(_LayoutFile)" Condition="$([System.String]::Copy("%(_LayoutFile.TargetPath)").StartsWith('CommunityToolkit.WinUI.Controls.Segmented\'))" />
+
+
+
diff --git a/components/ColorPicker/src/Converters/AccentColorConverter.cs b/components/ColorPicker/src/Converters/AccentColorConverter.cs
new file mode 100644
index 00000000..502cfc71
--- /dev/null
+++ b/components/ColorPicker/src/Converters/AccentColorConverter.cs
@@ -0,0 +1,122 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation 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 System.Globalization;
+using Windows.UI;
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// Creates an accent color for a base color value.
+///
+public class AccentColorConverter : IValueConverter
+{
+ ///
+ /// The amount to change the Value channel for each accent color step.
+ ///
+ public const double ValueDelta = 0.1;
+
+ ///
+ /// This does not account for perceptual differences and also does not match with
+ /// system accent color calculation.
+ ///
+ ///
+ /// Use the HSV representation as it's more perceptual.
+ /// In most cases only the value is changed by a fixed percentage so the algorithm is reproducible.
+ ///
+ /// The base color to calculate the accent from.
+ /// The number of accent color steps to move.
+ /// The new accent color.
+ public static HsvColor GetAccent(HsvColor hsvColor, int accentStep)
+ {
+ if (accentStep != 0)
+ {
+ double colorValue = hsvColor.V;
+ colorValue += accentStep * AccentColorConverter.ValueDelta;
+ colorValue = Math.Round(colorValue, 2);
+
+ return new HsvColor()
+ {
+ A = Math.Clamp(hsvColor.A, 0.0, 1.0),
+ H = Math.Clamp(hsvColor.H, 0.0, 360.0),
+ S = Math.Clamp(hsvColor.S, 0.0, 1.0),
+ V = Math.Clamp(colorValue, 0.0, 1.0),
+ };
+ }
+ else
+ {
+ return hsvColor;
+ }
+ }
+
+ ///
+ public object Convert(
+ object value,
+ Type targetType,
+ object parameter,
+ string language)
+ {
+ int accentStep;
+ Color? rgbColor = null;
+ HsvColor? hsvColor = null;
+
+ // Get the current color in HSV
+ if (value is Color valueColor)
+ {
+ rgbColor = valueColor;
+ }
+ else if (value is HsvColor valueHsvColor)
+ {
+ hsvColor = valueHsvColor;
+ }
+ else if (value is SolidColorBrush valueBrush)
+ {
+ rgbColor = valueBrush.Color;
+ }
+ else
+ {
+ // Invalid color value provided
+ return DependencyProperty.UnsetValue;
+ }
+
+ // Get the value component delta
+ try
+ {
+ accentStep = int.Parse(parameter?.ToString()!, CultureInfo.InvariantCulture);
+ }
+ catch
+ {
+ // Invalid parameter provided, unable to convert to integer
+ return DependencyProperty.UnsetValue;
+ }
+
+ if (hsvColor == null &&
+ rgbColor != null)
+ {
+ hsvColor = rgbColor.Value.ToHsv();
+ }
+
+ if (hsvColor != null)
+ {
+ var hsv = AccentColorConverter.GetAccent(hsvColor.Value, accentStep);
+
+ return Helpers.ColorHelper.FromHsv(hsv.H, hsv.S, hsv.V, hsv.A);
+ }
+ else
+ {
+ return DependencyProperty.UnsetValue;
+ }
+ }
+
+ ///
+ public object ConvertBack(
+ object value,
+ Type targetType,
+ object parameter,
+ string language)
+ {
+ return DependencyProperty.UnsetValue;
+ }
+}
diff --git a/components/ColorPicker/src/Converters/ColorToHexConverter.cs b/components/ColorPicker/src/Converters/ColorToHexConverter.cs
new file mode 100644
index 00000000..db6eec76
--- /dev/null
+++ b/components/ColorPicker/src/Converters/ColorToHexConverter.cs
@@ -0,0 +1,76 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation 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 CommunityToolkit.WinUI.Controls;
+
+///
+/// Converts a color to a hex string and vice versa.
+///
+public class ColorToHexConverter : IValueConverter
+{
+ ///
+ public object Convert(
+ object value,
+ Type targetType,
+ object parameter,
+ string language)
+ {
+ Color color;
+
+ if (value is Color valueColor)
+ {
+ color = valueColor;
+ }
+ else if (value is SolidColorBrush valueBrush)
+ {
+ color = valueBrush.Color;
+ }
+ else
+ {
+ // Invalid color value provided
+ return DependencyProperty.UnsetValue;
+ }
+
+ string hexColor = color.ToHex().Replace("#", string.Empty);
+ return hexColor;
+ }
+
+ ///
+ public object ConvertBack(
+ object value,
+ Type targetType,
+ object? parameter,
+ string? language)
+ {
+ string hexValue = value.ToString()!;
+
+ if (hexValue.StartsWith("#"))
+ {
+ try
+ {
+ return hexValue.ToColor();
+ }
+ catch
+ {
+ // Invalid hex color value provided
+ return DependencyProperty.UnsetValue;
+ }
+ }
+ else
+ {
+ try
+ {
+ return ("#" + hexValue).ToColor();
+ }
+ catch
+ {
+ // Invalid hex color value provided
+ return DependencyProperty.UnsetValue;
+ }
+ }
+ }
+}
diff --git a/components/ColorPicker/src/Converters/ContrastBrushConverter.cs b/components/ColorPicker/src/Converters/ContrastBrushConverter.cs
new file mode 100644
index 00000000..faf4a117
--- /dev/null
+++ b/components/ColorPicker/src/Converters/ContrastBrushConverter.cs
@@ -0,0 +1,122 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Windows.UI;
+
+#if WINAPPSDK
+using Colors = Microsoft.UI.Colors;
+#else
+using Colors = Windows.UI.Colors;
+#endif
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// Gets a color, either black or white, depending on the brightness of the supplied color.
+///
+public 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
+ if (this.UseLightContrastColor(comparisonColor))
+ {
+ return new SolidColorBrush(Colors.White);
+ }
+ else
+ {
+ return 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.
+ double rg = displayedColor.R <= 10 ? displayedColor.R / 3294.0 : Math.Pow((displayedColor.R / 269.0) + 0.0513, 2.4);
+ double gg = displayedColor.G <= 10 ? displayedColor.G / 3294.0 : Math.Pow((displayedColor.G / 269.0) + 0.0513, 2.4);
+ double 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/components/ColorPicker/src/Dependencies.props b/components/ColorPicker/src/Dependencies.props
new file mode 100644
index 00000000..e622e1df
--- /dev/null
+++ b/components/ColorPicker/src/Dependencies.props
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/ColorPicker/src/FluentColorPalette.cs b/components/ColorPicker/src/FluentColorPalette.cs
new file mode 100644
index 00000000..a8deef98
--- /dev/null
+++ b/components/ColorPicker/src/FluentColorPalette.cs
@@ -0,0 +1,173 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Color = Windows.UI.Color; // Type can be changed to CoreColor, etc.
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// Implements the standard Windows 10 color palette.
+///
+public class FluentColorPalette : IColorPalette
+{
+ /* Values were taken from the Settings App, Personalization > Colors which match with
+ * https://docs.microsoft.com/en-us/windows/uwp/whats-new/windows-docs-december-2017
+ *
+ * The default ordering and grouping of colors was undesirable so was modified.
+ * Colors were transposed: the colors in rows within the Settings app became columns here.
+ * This is because columns in an IColorPalette generally should contain different shades of
+ * the same color. In the settings app this concept is somewhat loosely reversed.
+ * The first 'column' ordering, after being transposed, was then reversed so 'red' colors
+ * were near to each other.
+ *
+ * This new ordering most closely follows the Windows standard while:
+ *
+ * 1. Keeping colors in a 'spectrum' order
+ * 2. Keeping like colors next to each both in rows and columns
+ * (which is unique for the windows palette).
+ * For example, similar red colors are next to each other in both
+ * rows within the same column and rows within the column next to it.
+ * This follows a 'snake-like' pattern as illustrated below.
+ * 3. A downside of this ordering is colors don't follow strict 'shades'
+ * as in other palettes.
+ *
+ * The colors will be displayed in the below pattern.
+ * This pattern follows a spectrum while keeping like-colors near to one
+ * another across both rows and columns.
+ *
+ * ┌Red───┐ ┌Blue──┐ ┌Gray──┐
+ * │ │ │ │ │ |
+ * │ │ │ │ │ |
+ * Yellow └Violet┘ └Green─┘ Brown
+ */
+ private static Color[,] colorChart = new Color[,]
+ {
+ {
+ // Ordering reversed for this section only
+ Color.FromArgb(255, 255, 185, 0), /* #ffb900 */
+ Color.FromArgb(255, 209, 52, 56), /* #d13438 */
+ Color.FromArgb(255, 227, 0, 140), /* #e3008c */
+ Color.FromArgb(255, 142, 140, 216), /* #8e8cd8 */
+ Color.FromArgb(255, 0, 153, 188), /* #0099bc */
+ Color.FromArgb(255, 0, 204, 106), /* #00cc6a */
+ Color.FromArgb(255, 86, 124, 115), /* #567c73 */
+ Color.FromArgb(255, 105, 121, 126), /* #69797e */
+ },
+ {
+ Color.FromArgb(255, 255, 140, 0), /* #ff8c00 */
+ Color.FromArgb(255, 255, 67, 67), /* #ff4343 */
+ Color.FromArgb(255, 191, 0, 119), /* #bf0077 */
+ Color.FromArgb(255, 107, 105, 214), /* #6b69d6 */
+ Color.FromArgb(255, 45, 125, 154), /* #2d7d9a */
+ Color.FromArgb(255, 16, 137, 62), /* #10893e */
+ Color.FromArgb(255, 72, 104, 96), /* #486860 */
+ Color.FromArgb(255, 74, 84, 89), /* #4a5459 */
+ },
+ {
+ Color.FromArgb(255, 247, 99, 12), /* #f7630c */
+ Color.FromArgb(255, 231, 72, 86), /* #e74856 */
+ Color.FromArgb(255, 194, 57, 179), /* #c239b3 */
+ Color.FromArgb(255, 135, 100, 184), /* #8764b8 */
+ Color.FromArgb(255, 0, 183, 195), /* #00b7c3 */
+ Color.FromArgb(255, 122, 117, 116), /* #7a7574 */
+ Color.FromArgb(255, 73, 130, 5), /* #498205 */
+ Color.FromArgb(255, 100, 124, 100), /* #647c64 */
+ },
+ {
+ Color.FromArgb(255, 202, 80, 16), /* #ca5010 */
+ Color.FromArgb(255, 232, 17, 35), /* #e81123 */
+ Color.FromArgb(255, 154, 0, 137), /* #9a0089 */
+ Color.FromArgb(255, 116, 77, 169), /* #744da9 */
+ Color.FromArgb(255, 3, 131, 135), /* #038387 */
+ Color.FromArgb(255, 93, 90, 88), /* #5d5a58 */
+ Color.FromArgb(255, 16, 124, 16), /* #107c10 */
+ Color.FromArgb(255, 82, 94, 84), /* #525e54 */
+ },
+ {
+ Color.FromArgb(255, 218, 59, 1), /* #da3b01 */
+ Color.FromArgb(255, 234, 0, 94), /* #ea005e */
+ Color.FromArgb(255, 0, 120, 212), /* #0078d4 */
+ Color.FromArgb(255, 177, 70, 194), /* #b146c2 */
+ Color.FromArgb(255, 0, 178, 148), /* #00b294 */
+ Color.FromArgb(255, 104, 118, 138), /* #68768a */
+ Color.FromArgb(255, 118, 118, 118), /* #767676 */
+ Color.FromArgb(255, 132, 117, 69), /* #847545 */
+ },
+ {
+ Color.FromArgb(255, 239, 105, 80), /* #ef6950 */
+ Color.FromArgb(255, 195, 0, 82), /* #c30052 */
+ Color.FromArgb(255, 0, 99, 177), /* #0063b1 */
+ Color.FromArgb(255, 136, 23, 152), /* #881798 */
+ Color.FromArgb(255, 1, 133, 116), /* #018574 */
+ Color.FromArgb(255, 81, 92, 107), /* #515c6b */
+ Color.FromArgb(255, 76, 74, 72), /* #4c4a48 */
+ Color.FromArgb(255, 126, 115, 95), /* #7e735f */
+ }
+ };
+
+ /***************************************************************************************
+ *
+ * Color Indexes
+ *
+ ***************************************************************************************/
+
+ ///
+ /// Gets the index of the default shade of colors in this palette.
+ /// This has little meaning in this palette as colors are not strictly separated by shade.
+ ///
+ public const int DefaultShadeIndex = 0;
+
+ /***************************************************************************************
+ *
+ * Property Accessors
+ *
+ ***************************************************************************************/
+
+ ///////////////////////////////////////////////////////////
+ // Palette
+ ///////////////////////////////////////////////////////////
+
+ ///
+ /// Gets the total number of colors in this palette.
+ /// A color is not necessarily a single value and may be composed of several shades.
+ /// This has little meaning in this palette as colors are not strictly separated.
+ ///
+ public int ColorCount
+ {
+ get { return colorChart.GetLength(0); }
+ }
+
+ ///
+ /// Gets the total number of shades for each color in this palette.
+ /// Shades are usually a variation of the color lightening or darkening it.
+ /// This has little meaning in this palette as colors are not strictly separated by shade.
+ ///
+ public int ShadeCount
+ {
+ get { return colorChart.GetLength(1); }
+ }
+
+ /***************************************************************************************
+ *
+ * Methods
+ *
+ ***************************************************************************************/
+
+ ///
+ /// Gets a color in the palette by index.
+ ///
+ /// The index of the color in the palette.
+ /// The index must be between zero and .
+ /// The index of the color shade in the palette.
+ /// The index must be between zero and .
+ /// The color at the specified index or an exception.
+ public Color GetColor(
+ int colorIndex,
+ int shadeIndex)
+ {
+ return colorChart[
+ Math.Clamp(colorIndex, 0, colorChart.GetLength(0)),
+ Math.Clamp(shadeIndex, 0, colorChart.GetLength(1))];
+ }
+}
diff --git a/components/ColorPicker/src/IColorPalette.cs b/components/ColorPicker/src/IColorPalette.cs
new file mode 100644
index 00000000..4f8a9cf3
--- /dev/null
+++ b/components/ColorPicker/src/IColorPalette.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Windows.UI;
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// Interface to define a color palette.
+///
+public interface IColorPalette
+{
+ ///
+ /// Gets the total number of colors in this palette.
+ /// A color is not necessarily a single value and may be composed of several shades.
+ ///
+ int ColorCount { get; }
+
+ ///
+ /// Gets the total number of shades for each color in this palette.
+ /// Shades are usually a variation of the color lightening or darkening it.
+ ///
+ int ShadeCount { get; }
+
+ ///
+ /// Gets a color in the palette by index.
+ ///
+ /// The index of the color in the palette.
+ /// The index must be between zero and .
+ /// The index of the color shade in the palette.
+ /// The index must be between zero and .
+ /// The color at the specified index or an exception.
+ Color GetColor(int colorIndex, int shadeIndex);
+}
diff --git a/components/ColorPicker/src/MultiTarget.props b/components/ColorPicker/src/MultiTarget.props
new file mode 100644
index 00000000..b11c1942
--- /dev/null
+++ b/components/ColorPicker/src/MultiTarget.props
@@ -0,0 +1,9 @@
+
+
+
+ uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android;
+
+
\ No newline at end of file
diff --git a/components/ColorPicker/src/Themes/Generic.xaml b/components/ColorPicker/src/Themes/Generic.xaml
new file mode 100644
index 00000000..41854e9f
--- /dev/null
+++ b/components/ColorPicker/src/Themes/Generic.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/components/ColorPicker/tests/ColorPicker.Tests.projitems b/components/ColorPicker/tests/ColorPicker.Tests.projitems
new file mode 100644
index 00000000..032cb653
--- /dev/null
+++ b/components/ColorPicker/tests/ColorPicker.Tests.projitems
@@ -0,0 +1,23 @@
+
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ true
+ 7241E39A-E282-42BF-A260-30BF986D876D
+
+
+ ColorPickerExperiment.Tests
+
+
+
+
+ ExampleColorPickerTestPage.xaml
+
+
+
+
+ Designer
+ MSBuild:Compile
+
+
+
\ No newline at end of file
diff --git a/components/ColorPicker/tests/ColorPicker.Tests.shproj b/components/ColorPicker/tests/ColorPicker.Tests.shproj
new file mode 100644
index 00000000..df238853
--- /dev/null
+++ b/components/ColorPicker/tests/ColorPicker.Tests.shproj
@@ -0,0 +1,13 @@
+
+
+
+ 7241E39A-E282-42BF-A260-30BF986D876D
+ 14.0
+
+
+
+
+
+
+
+
diff --git a/components/ColorPicker/tests/ExampleColorPickerTestClass.cs b/components/ColorPicker/tests/ExampleColorPickerTestClass.cs
new file mode 100644
index 00000000..01e03403
--- /dev/null
+++ b/components/ColorPicker/tests/ExampleColorPickerTestClass.cs
@@ -0,0 +1,135 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Tooling.TestGen;
+using CommunityToolkit.Tests;
+using CommunityToolkit.WinUI.Controls;
+using ColorPicker = CommunityToolkit.WinUI.Controls.ColorPicker;
+
+namespace ColorPickerExperiment.Tests;
+
+[TestClass]
+public partial class ExampleColorPickerTestClass : VisualUITestBase
+{
+ // If you don't need access to UI objects directly or async code, use this pattern.
+ [TestMethod]
+ public void SimpleSynchronousExampleTest()
+ {
+ var assembly = typeof(ColorPicker).Assembly;
+ var type = assembly.GetType(typeof(ColorPicker).FullName ?? string.Empty);
+
+ Assert.IsNotNull(type, "Could not find ColorPicker type.");
+ Assert.AreEqual(typeof(ColorPicker), type, "Type of ColorPicker does not match expected type.");
+ }
+
+ // If you don't need access to UI objects directly, use this pattern.
+ [TestMethod]
+ public async Task SimpleAsyncExampleTest()
+ {
+ await Task.Delay(250);
+
+ Assert.IsTrue(true);
+ }
+
+ // Example that shows how to check for exception throwing.
+ [TestMethod]
+ public void SimpleExceptionCheckTest()
+ {
+ // If you need to check exceptions occur for invalid inputs, etc...
+ // Use Assert.ThrowsException to limit the scope to where you expect the error to occur.
+ // Otherwise, using the ExpectedException attribute could swallow or
+ // catch other issues in setup code.
+ Assert.ThrowsException(() => throw new NotImplementedException());
+ }
+
+ // The UIThreadTestMethod automatically dispatches to the UI for us to work with UI objects.
+ [UIThreadTestMethod]
+ public void SimpleUIAttributeExampleTest()
+ {
+ var component = new ColorPicker();
+ Assert.IsNotNull(component);
+ }
+
+ // The UIThreadTestMethod can also easily grab a XAML Page for us by passing its type as a parameter.
+ // This lets us actually test a control as it would behave within an actual application.
+ // The page will already be loaded by the time your test is called.
+ [UIThreadTestMethod]
+ public void SimpleUIExamplePageTest(ExampleColorPickerTestPage page)
+ {
+ // You can use the Toolkit Visual Tree helpers here to find the component by type or name:
+ var component = page.FindDescendant();
+
+ Assert.IsNotNull(component);
+
+ var componentByName = page.FindDescendant("ColorPickerControl");
+
+ Assert.IsNotNull(componentByName);
+ }
+
+ // You can still do async work with a UIThreadTestMethod as well.
+ [UIThreadTestMethod]
+ public async Task SimpleAsyncUIExamplePageTest(ExampleColorPickerTestPage page)
+ {
+ // This helper can be used to wait for a rendering pass to complete.
+ // Note, this is already done by loading a Page with the [UIThreadTestMethod] helper.
+ await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { });
+
+ var component = page.FindDescendant();
+
+ Assert.IsNotNull(component);
+ }
+
+ //// ----------------------------- ADVANCED TEST SCENARIOS -----------------------------
+
+ // If you need to use DataRow, you can use this pattern with the UI dispatch still.
+ // Otherwise, checkout the UIThreadTestMethod attribute above.
+ // See https://github.com/CommunityToolkit/Labs-Windows/issues/186
+ [TestMethod]
+ public async Task ComplexAsyncUIExampleTest()
+ {
+ await EnqueueAsync(() =>
+ {
+ var component = new ColorPicker();
+ Assert.IsNotNull(component);
+ });
+ }
+
+ // If you want to load other content not within a XAML page using the UIThreadTestMethod above.
+ // Then you can do that using the Load/UnloadTestContentAsync methods.
+ [TestMethod]
+ public async Task ComplexAsyncLoadUIExampleTest()
+ {
+ await EnqueueAsync(async () =>
+ {
+ var component = new ColorPicker();
+ Assert.IsNotNull(component);
+ Assert.IsFalse(component.IsLoaded);
+
+ await LoadTestContentAsync(component);
+
+ Assert.IsTrue(component.IsLoaded);
+
+ await UnloadTestContentAsync(component);
+
+ Assert.IsFalse(component.IsLoaded);
+ });
+ }
+
+ // You can still use the UIThreadTestMethod to remove the extra layer for the dispatcher as well:
+ [UIThreadTestMethod]
+ public async Task ComplexAsyncLoadUIExampleWithoutDispatcherTest()
+ {
+ var component = new ColorPicker();
+ Assert.IsNotNull(component);
+ Assert.IsFalse(component.IsLoaded);
+
+ await LoadTestContentAsync(component);
+
+ Assert.IsTrue(component.IsLoaded);
+
+ await UnloadTestContentAsync(component);
+
+ Assert.IsFalse(component.IsLoaded);
+ }
+}
diff --git a/components/ColorPicker/tests/ExampleColorPickerTestPage.xaml b/components/ColorPicker/tests/ExampleColorPickerTestPage.xaml
new file mode 100644
index 00000000..5d68f92d
--- /dev/null
+++ b/components/ColorPicker/tests/ExampleColorPickerTestPage.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/components/ColorPicker/tests/ExampleColorPickerTestPage.xaml.cs b/components/ColorPicker/tests/ExampleColorPickerTestPage.xaml.cs
new file mode 100644
index 00000000..851294df
--- /dev/null
+++ b/components/ColorPicker/tests/ExampleColorPickerTestPage.xaml.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace ColorPickerExperiment.Tests;
+
+///
+/// An empty page that can be used on its own or navigated to within a Frame.
+///
+public sealed partial class ExampleColorPickerTestPage : Page
+{
+ public ExampleColorPickerTestPage()
+ {
+ this.InitializeComponent();
+ }
+}