diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj
index f28ed7f40c5..c163498a562 100644
--- a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj
+++ b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj
@@ -272,6 +272,7 @@
+
@@ -509,6 +510,12 @@
AutoFocusBehaviorPage.xaml
+
+ ColorPickerButtonPage.xaml
+
+
+ ColorPickerPage.xaml
+ EnumValuesExtensionPage.xaml
@@ -620,6 +627,8 @@
+
+
@@ -993,6 +1002,14 @@
DesignerMSBuild:Compile
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+ MSBuild:CompileDesigner
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPicker.png b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPicker.png
new file mode 100644
index 00000000000..9a821466c4e
Binary files /dev/null and b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPicker.png differ
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerButtonPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerButtonPage.xaml
new file mode 100644
index 00000000000..979f2b497d4
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerButtonPage.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerButtonPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerButtonPage.xaml.cs
new file mode 100644
index 00000000000..72e50046b8f
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerButtonPage.xaml.cs
@@ -0,0 +1,20 @@
+// 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 Microsoft.Toolkit.Uwp.UI.Controls;
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages
+{
+ ///
+ /// A page that shows how to use the control.
+ ///
+ public sealed partial class ColorPickerButtonPage : Page
+ {
+ public ColorPickerButtonPage()
+ {
+ this.InitializeComponent();
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerButtonXaml.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerButtonXaml.bind
new file mode 100644
index 00000000000..c06c3eb8e49
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerButtonXaml.bind
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Box-shaped spectrum
+ Alpha channel disabled
+
+
+
+
+
+
+
+
+
+
+
+
+ Box-shaped spectrum
+ Alpha channel enabled
+
+
+
+
+
+
+
+
+
+
+
+
+ Ring-shaped spectrum
+ Alpha channel enabled
+
+
+
+
+
+
+
+
+
+
+
+
+ Ring-shaped spectrum
+ Alpha channel enabled
+ Saturation+Value spectrum channels
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerPage.xaml
new file mode 100644
index 00000000000..f92683d69d6
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerPage.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerPage.xaml.cs
new file mode 100644
index 00000000000..15d297b2527
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerPage.xaml.cs
@@ -0,0 +1,20 @@
+// 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 Microsoft.Toolkit.Uwp.UI.Controls;
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages
+{
+ ///
+ /// A page that shows how to use the control.
+ ///
+ public sealed partial class ColorPickerPage : Page
+ {
+ public ColorPickerPage()
+ {
+ this.InitializeComponent();
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerXaml.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerXaml.bind
new file mode 100644
index 00000000000..a8a6d994f19
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ColorPicker/ColorPickerXaml.bind
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+ Box-shaped spectrum
+ Alpha channel disabled
+
+
+
+
+
+
+ Box-shaped spectrum
+ Alpha channel enabled
+
+
+
+
+
+
+ Ring-shaped spectrum
+ Alpha channel enabled
+
+
+
+
+
+
+ Ring-shaped spectrum
+ Alpha channel enabled
+ Saturation+Value spectrum channels
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
index 1a5c5303b40..dd893d84ea6 100644
--- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
@@ -34,6 +34,26 @@
"Icon": "/SamplePages/Carousel/Carousel.png",
"DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/controls/Carousel.md"
},
+ {
+ "Name": "ColorPicker",
+ "Type": "ColorPickerPage",
+ "Subcategory": "Input",
+ "About": "An improved color picker control providing more options to select colors.",
+ "CodeUrl": "https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker",
+ "XamlCodeFile": "ColorPickerXaml.bind",
+ "Icon": "/SamplePages/ColorPicker/ColorPicker.png",
+ "DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/controls/ColorPicker.md"
+ },
+ {
+ "Name": "ColorPickerButton",
+ "Type": "ColorPickerButtonPage",
+ "Subcategory": "Input",
+ "About": "A color picker within a flyout opened by pressing a dropdown button containing the selected color.",
+ "CodeUrl": "https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker",
+ "XamlCodeFile": "/SamplePages/ColorPicker/ColorPickerButtonXaml.bind",
+ "Icon": "/SamplePages/ColorPicker/ColorPicker.png",
+ "DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/controls/ColorPickerButton.md"
+ },
{
"Name": "AdaptiveGridView",
"Type": "AdaptiveGridViewPage",
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorChannel.cs b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorChannel.cs
new file mode 100644
index 00000000000..77df7b01c24
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorChannel.cs
@@ -0,0 +1,40 @@
+// 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;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.Toolkit.Uwp.UI.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/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPicker.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPicker.Properties.cs
new file mode 100644
index 00000000000..b21772a2ea9
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPicker.Properties.cs
@@ -0,0 +1,111 @@
+// 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.Collections.ObjectModel;
+using Windows.UI.Xaml;
+
+namespace Microsoft.Toolkit.Uwp.UI.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(Windows.UI.Color.FromArgb(0x00, 0x00, 0x00, 0x00)));
+
+ ///
+ /// 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));
+
+ ///
+ /// 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(DependencyProperty.UnsetValue));
+
+ ///
+ /// 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));
+
+ ///
+ /// 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);
+ }
+ }
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPicker.cs b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPicker.cs
new file mode 100644
index 00000000000..cb7398d9791
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPicker.cs
@@ -0,0 +1,1484 @@
+// 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;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Globalization;
+using Microsoft.Toolkit.Diagnostics;
+using Microsoft.Toolkit.Uwp.Helpers;
+using Microsoft.Toolkit.Uwp.UI.Controls.ColorPickerConverters;
+using Windows.Foundation;
+using Windows.UI;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Controls.Primitives;
+using Windows.UI.Xaml.Media;
+using ColorPickerSlider = Microsoft.Toolkit.Uwp.UI.Controls.Primitives.ColorPickerSlider;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ ///
+ /// Presents a color spectrum, a palette of colors, and color channel sliders for user selection of a color.
+ ///
+ [TemplatePart(Name = nameof(ColorPicker.AlphaChannelSlider), Type = typeof(ColorPickerSlider))]
+ [TemplatePart(Name = nameof(ColorPicker.AlphaChannelTextBox), Type = typeof(TextBox))]
+ [TemplatePart(Name = nameof(ColorPicker.Channel1Slider), Type = typeof(ColorPickerSlider))]
+ [TemplatePart(Name = nameof(ColorPicker.Channel1TextBox), Type = typeof(TextBox))]
+ [TemplatePart(Name = nameof(ColorPicker.Channel2Slider), Type = typeof(ColorPickerSlider))]
+ [TemplatePart(Name = nameof(ColorPicker.Channel2TextBox), Type = typeof(TextBox))]
+ [TemplatePart(Name = nameof(ColorPicker.Channel3Slider), Type = typeof(ColorPickerSlider))]
+ [TemplatePart(Name = nameof(ColorPicker.Channel3TextBox), Type = typeof(TextBox))]
+ [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.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.HsvToggleButton), Type = typeof(ToggleButton))]
+ [TemplatePart(Name = nameof(ColorPicker.RgbToggleButton), Type = typeof(ToggleButton))]
+ [TemplatePart(Name = nameof(ColorPicker.P1PreviewBorder), Type = typeof(Border))]
+ [TemplatePart(Name = nameof(ColorPicker.P2PreviewBorder), Type = typeof(Border))]
+ [TemplatePart(Name = nameof(ColorPicker.N1PreviewBorder), Type = typeof(Border))]
+ [TemplatePart(Name = nameof(ColorPicker.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 ColorPicker : Windows.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 long tokenCustomPalette;
+ private long tokenIsColorPaletteVisible;
+
+ 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 DispatcherTimer dispatcherTimer = null;
+
+ private ColorSpectrum ColorSpectrumControl;
+ private ColorPickerSlider ColorSpectrumAlphaSlider;
+ private ColorPickerSlider ColorSpectrumThirdDimensionSlider;
+ private TextBox HexInputTextBox;
+ private ToggleButton HsvToggleButton;
+ private ToggleButton RgbToggleButton;
+
+ private TextBox Channel1TextBox;
+ private TextBox Channel2TextBox;
+ private TextBox Channel3TextBox;
+ private TextBox AlphaChannelTextBox;
+ private ColorPickerSlider Channel1Slider;
+ private ColorPickerSlider Channel2Slider;
+ private ColorPickerSlider Channel3Slider;
+ private ColorPickerSlider AlphaChannelSlider;
+
+ private Border N1PreviewBorder;
+ private Border N2PreviewBorder;
+ private Border P1PreviewBorder;
+ private Border P2PreviewBorder;
+
+ // 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.
+ ///
+ public ColorPicker()
+ {
+ this.DefaultStyleKey = typeof(ColorPicker);
+
+ // 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.StartDispatcherTimer();
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~ColorPicker()
+ {
+ this.StopDispatcherTimer();
+ 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.ColorSpectrumControl = this.GetTemplateChild(nameof(ColorSpectrumControl));
+ this.ColorSpectrumAlphaSlider = this.GetTemplateChild(nameof(ColorSpectrumAlphaSlider));
+ this.ColorSpectrumThirdDimensionSlider = this.GetTemplateChild(nameof(ColorSpectrumThirdDimensionSlider));
+
+ this.HexInputTextBox = this.GetTemplateChild(nameof(HexInputTextBox));
+ this.HsvToggleButton = this.GetTemplateChild(nameof(HsvToggleButton));
+ this.RgbToggleButton = this.GetTemplateChild(nameof(RgbToggleButton));
+
+ this.Channel1TextBox = this.GetTemplateChild(nameof(Channel1TextBox));
+ this.Channel2TextBox = this.GetTemplateChild(nameof(Channel2TextBox));
+ this.Channel3TextBox = this.GetTemplateChild(nameof(Channel3TextBox));
+ this.AlphaChannelTextBox = this.GetTemplateChild(nameof(AlphaChannelTextBox));
+
+ this.Channel1Slider = this.GetTemplateChild(nameof(Channel1Slider));
+ this.Channel2Slider = this.GetTemplateChild(nameof(Channel2Slider));
+ this.Channel3Slider = this.GetTemplateChild(nameof(Channel3Slider));
+ this.AlphaChannelSlider = this.GetTemplateChild(nameof(AlphaChannelSlider));
+
+ this.N1PreviewBorder = this.GetTemplateChild(nameof(N1PreviewBorder));
+ this.N2PreviewBorder = this.GetTemplateChild(nameof(N2PreviewBorder));
+ this.P1PreviewBorder = this.GetTemplateChild(nameof(P1PreviewBorder));
+ this.P2PreviewBorder = this.GetTemplateChild(nameof(P2PreviewBorder));
+
+ this.CheckeredBackground1Border = this.GetTemplateChild(nameof(CheckeredBackground1Border));
+ this.CheckeredBackground2Border = this.GetTemplateChild(nameof(CheckeredBackground2Border));
+ this.CheckeredBackground3Border = this.GetTemplateChild(nameof(CheckeredBackground3Border));
+ this.CheckeredBackground4Border = this.GetTemplateChild(nameof(CheckeredBackground4Border));
+ this.CheckeredBackground5Border = this.GetTemplateChild(nameof(CheckeredBackground5Border));
+ this.CheckeredBackground6Border = this.GetTemplateChild(nameof(CheckeredBackground6Border));
+ this.CheckeredBackground7Border = this.GetTemplateChild(nameof(CheckeredBackground7Border));
+ this.CheckeredBackground8Border = this.GetTemplateChild(nameof(CheckeredBackground8Border));
+ this.CheckeredBackground9Border = this.GetTemplateChild(nameof(CheckeredBackground9Border));
+ this.CheckeredBackground10Border = this.GetTemplateChild(nameof(CheckeredBackground10Border));
+
+ // Must connect after controls are resolved
+ this.ConnectEvents(true);
+
+ base.OnApplyTemplate();
+ this.UpdateVisualState(false);
+ 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?
+ }
+
+ ///
+ /// Retrieves the named element in the instantiated ControlTemplate visual tree.
+ ///
+ /// The name of the element to find.
+ /// Whether the element is required and will throw an exception if missing.
+ /// The template child matching the given name and type.
+ private T GetTemplateChild(string childName, bool isRequired = false)
+ where T : DependencyObject
+ {
+ T child = this.GetTemplateChild(childName) as T;
+ if ((child == null) && isRequired)
+ {
+ ThrowHelper.ThrowArgumentNullException(childName);
+ }
+
+ return child;
+ }
+
+ ///
+ /// Connects or disconnects all dependency property callbacks.
+ ///
+ /// True to connect callbacks, otherwise false.
+ private void ConnectCallbacks(bool connected)
+ {
+ if ((connected == true) &&
+ (this.callbacksConnected == false))
+ {
+ // Add callbacks for dependency properties
+ this.tokenColor = this.RegisterPropertyChangedCallback(ColorProperty, OnColorChanged);
+ this.tokenCustomPalette = this.RegisterPropertyChangedCallback(CustomPaletteProperty, OnCustomPaletteChanged);
+ this.tokenIsColorPaletteVisible = this.RegisterPropertyChangedCallback(IsColorPaletteVisibleProperty, OnIsColorPaletteVisibleChanged);
+
+ this.callbacksConnected = true;
+ }
+ else if ((connected == false) &&
+ (this.callbacksConnected == true))
+ {
+ // Remove callbacks for dependency properties
+ this.UnregisterPropertyChangedCallback(ColorProperty, this.tokenColor);
+ this.UnregisterPropertyChangedCallback(CustomPaletteProperty, this.tokenCustomPalette);
+ this.UnregisterPropertyChangedCallback(IsColorPaletteVisibleProperty, this.tokenIsColorPaletteVisible);
+
+ 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.HsvToggleButton != null) { this.HsvToggleButton.Checked += ColorRepToggleButton_CheckedUnchecked; }
+ if (this.HsvToggleButton != null) { this.HsvToggleButton.Unchecked += ColorRepToggleButton_CheckedUnchecked; }
+ if (this.RgbToggleButton != null) { this.RgbToggleButton.Checked += ColorRepToggleButton_CheckedUnchecked; }
+ if (this.RgbToggleButton != null) { this.RgbToggleButton.Unchecked += ColorRepToggleButton_CheckedUnchecked; }
+
+ if (this.Channel1TextBox != null) { this.Channel1TextBox.KeyDown += ChannelTextBox_KeyDown; }
+ if (this.Channel2TextBox != null) { this.Channel2TextBox.KeyDown += ChannelTextBox_KeyDown; }
+ if (this.Channel3TextBox != null) { this.Channel3TextBox.KeyDown += ChannelTextBox_KeyDown; }
+ if (this.AlphaChannelTextBox != null) { this.AlphaChannelTextBox.KeyDown += ChannelTextBox_KeyDown; }
+ if (this.Channel1TextBox != null) { this.Channel1TextBox.LostFocus += ChannelTextBox_LostFocus; }
+ if (this.Channel2TextBox != null) { this.Channel2TextBox.LostFocus += ChannelTextBox_LostFocus; }
+ if (this.Channel3TextBox != null) { this.Channel3TextBox.LostFocus += ChannelTextBox_LostFocus; }
+ if (this.AlphaChannelTextBox != null) { this.AlphaChannelTextBox.LostFocus += ChannelTextBox_LostFocus; }
+
+ 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.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; }
+
+ 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.HsvToggleButton != null) { this.HsvToggleButton.Checked -= ColorRepToggleButton_CheckedUnchecked; }
+ if (this.HsvToggleButton != null) { this.HsvToggleButton.Unchecked -= ColorRepToggleButton_CheckedUnchecked; }
+ if (this.RgbToggleButton != null) { this.RgbToggleButton.Checked -= ColorRepToggleButton_CheckedUnchecked; }
+ if (this.RgbToggleButton != null) { this.RgbToggleButton.Unchecked -= ColorRepToggleButton_CheckedUnchecked; }
+
+ if (this.Channel1TextBox != null) { this.Channel1TextBox.KeyDown -= ChannelTextBox_KeyDown; }
+ if (this.Channel2TextBox != null) { this.Channel2TextBox.KeyDown -= ChannelTextBox_KeyDown; }
+ if (this.Channel3TextBox != null) { this.Channel3TextBox.KeyDown -= ChannelTextBox_KeyDown; }
+ if (this.AlphaChannelTextBox != null) { this.AlphaChannelTextBox.KeyDown -= ChannelTextBox_KeyDown; }
+ if (this.Channel1TextBox != null) { this.Channel1TextBox.LostFocus -= ChannelTextBox_LostFocus; }
+ if (this.Channel2TextBox != null) { this.Channel2TextBox.LostFocus -= ChannelTextBox_LostFocus; }
+ if (this.Channel3TextBox != null) { this.Channel3TextBox.LostFocus -= ChannelTextBox_LostFocus; }
+ if (this.AlphaChannelTextBox != null) { this.AlphaChannelTextBox.LostFocus -= ChannelTextBox_LostFocus; }
+
+ 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.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; }
+
+ 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)
+ {
+ 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);
+
+ return;
+ }
+
+ ///
+ /// 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.HsvToggleButton != null &&
+ this.HsvToggleButton.IsChecked == true)
+ {
+ 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)
+ {
+ if (this.RgbToggleButton != null &&
+ (bool)this.RgbToggleButton.IsChecked)
+ {
+ this.RgbToggleButton.IsChecked = false;
+ }
+
+ if (this.HsvToggleButton != null &&
+ (bool)this.HsvToggleButton.IsChecked == false)
+ {
+ this.HsvToggleButton.IsChecked = true;
+ }
+ }
+ else
+ {
+ if (this.RgbToggleButton != null &&
+ (bool)this.RgbToggleButton.IsChecked == false)
+ {
+ this.RgbToggleButton.IsChecked = true;
+ }
+
+ if (this.HsvToggleButton != null &&
+ (bool)this.HsvToggleButton.IsChecked)
+ {
+ this.HsvToggleButton.IsChecked = false;
+ }
+ }
+
+ 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 Windows.UI.Xaml.Controls.ColorSpectrumComponents.SaturationValue:
+ case Windows.UI.Xaml.Controls.ColorSpectrumComponents.ValueSaturation:
+ {
+ // Hue
+ return ColorChannel.Channel1;
+ }
+
+ case Windows.UI.Xaml.Controls.ColorSpectrumComponents.HueValue:
+ case Windows.UI.Xaml.Controls.ColorSpectrumComponents.ValueHue:
+ {
+ // Saturation
+ return ColorChannel.Channel2;
+ }
+
+ case Windows.UI.Xaml.Controls.ColorSpectrumComponents.HueSaturation:
+ case Windows.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;
+ }
+
+ ///
+ /// Applies the value of the given color channel TextBox to the current color.
+ ///
+ /// The color channel TextBox to apply the value from.
+ private void ApplyChannelTextBoxValue(TextBox channelTextBox)
+ {
+ double channelValue;
+
+ if (channelTextBox != null)
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(channelTextBox.Text))
+ {
+ // An empty string is allowed and happens when the clear TextBox button is pressed
+ // This case should be interpreted as zero
+ channelValue = 0;
+ }
+ else
+ {
+ channelValue = double.Parse(channelTextBox.Text, CultureInfo.CurrentUICulture);
+ }
+
+ if (object.ReferenceEquals(channelTextBox, this.Channel1TextBox))
+ {
+ this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel1, channelValue);
+ }
+ else if (object.ReferenceEquals(channelTextBox, this.Channel2TextBox))
+ {
+ this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel2, channelValue);
+ }
+ else if (object.ReferenceEquals(channelTextBox, this.Channel3TextBox))
+ {
+ this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel3, channelValue);
+ }
+ else if (object.ReferenceEquals(channelTextBox, this.AlphaChannelTextBox))
+ {
+ this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Alpha, channelValue);
+ }
+ }
+ catch
+ {
+ // Reset TextBox values
+ this.UpdateColorControlValues();
+ this.UpdateChannelSliderBackgrounds();
+ }
+ }
+
+ 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 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.Channel1TextBox != null)
+ {
+ this.Channel1TextBox.MaxLength = 3;
+ this.Channel1TextBox.Text = hue.ToString(CultureInfo.CurrentUICulture);
+ }
+
+ if (this.Channel1Slider != null)
+ {
+ this.Channel1Slider.Minimum = 0;
+ this.Channel1Slider.Maximum = 360;
+ this.Channel1Slider.Value = hue;
+ }
+
+ // Saturation
+ if (this.Channel2TextBox != null)
+ {
+ this.Channel2TextBox.MaxLength = 3;
+ this.Channel2TextBox.Text = staturation.ToString(CultureInfo.CurrentUICulture);
+ }
+
+ if (this.Channel2Slider != null)
+ {
+ this.Channel2Slider.Minimum = 0;
+ this.Channel2Slider.Maximum = 100;
+ this.Channel2Slider.Value = staturation;
+ }
+
+ // Value
+ if (this.Channel3TextBox != null)
+ {
+ this.Channel3TextBox.MaxLength = 3;
+ this.Channel3TextBox.Text = value.ToString(CultureInfo.CurrentUICulture);
+ }
+
+ if (this.Channel3Slider != null)
+ {
+ this.Channel3Slider.Minimum = 0;
+ this.Channel3Slider.Maximum = 100;
+ this.Channel3Slider.Value = value;
+ }
+
+ // Alpha
+ if (this.AlphaChannelTextBox != null)
+ {
+ this.AlphaChannelTextBox.MaxLength = 3;
+ this.AlphaChannelTextBox.Text = alpha.ToString(CultureInfo.CurrentUICulture);
+ }
+
+ 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.Channel1TextBox != null)
+ {
+ this.Channel1TextBox.MaxLength = 3;
+ this.Channel1TextBox.Text = rgbColor.R.ToString(CultureInfo.CurrentUICulture);
+ }
+
+ if (this.Channel1Slider != null)
+ {
+ this.Channel1Slider.Minimum = 0;
+ this.Channel1Slider.Maximum = 255;
+ this.Channel1Slider.Value = Convert.ToDouble(rgbColor.R);
+ }
+
+ // Green
+ if (this.Channel2TextBox != null)
+ {
+ this.Channel2TextBox.MaxLength = 3;
+ this.Channel2TextBox.Text = rgbColor.G.ToString(CultureInfo.CurrentUICulture);
+ }
+
+ if (this.Channel2Slider != null)
+ {
+ this.Channel2Slider.Minimum = 0;
+ this.Channel2Slider.Maximum = 255;
+ this.Channel2Slider.Value = Convert.ToDouble(rgbColor.G);
+ }
+
+ // Blue
+ if (this.Channel3TextBox != null)
+ {
+ this.Channel3TextBox.MaxLength = 3;
+ this.Channel3TextBox.Text = rgbColor.B.ToString(CultureInfo.CurrentUICulture);
+ }
+
+ if (this.Channel3Slider != null)
+ {
+ this.Channel3Slider.Minimum = 0;
+ this.Channel3Slider.Maximum = 255;
+ this.Channel3Slider.Value = Convert.ToDouble(rgbColor.B);
+ }
+
+ // Alpha
+ if (this.AlphaChannelTextBox != null)
+ {
+ this.AlphaChannelTextBox.MaxLength = 3;
+ this.AlphaChannelTextBox.Text = rgbColor.A.ToString(CultureInfo.CurrentUICulture);
+ }
+
+ 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 = Uwp.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;
+ }
+
+ /***************************************************************************************
+ *
+ * Color Update Timer
+ *
+ ***************************************************************************************/
+
+ private void StartDispatcherTimer()
+ {
+ this.dispatcherTimer = new DispatcherTimer()
+ {
+ Interval = new TimeSpan(0, 0, 0, 0, ColorUpdateInterval)
+ };
+ this.dispatcherTimer.Tick += DispatcherTimer_Tick;
+ this.dispatcherTimer.Start();
+
+ return;
+ }
+
+ private void StopDispatcherTimer()
+ {
+ if (this.dispatcherTimer != null)
+ {
+ this.dispatcherTimer.Stop();
+ }
+
+ return;
+ }
+
+ private void DispatcherTimer_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;
+ }
+
+ ///
+ /// Callback for when the dependency property value changes.
+ ///
+ private void OnCustomPaletteChanged(DependencyObject d, DependencyProperty e)
+ {
+ 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));
+ }
+ }
+ }
+
+ return;
+ }
+
+ ///
+ /// Callback for when the dependency property value changes.
+ ///
+ private void OnIsColorPaletteVisibleChanged(DependencyObject d, DependencyProperty e)
+ {
+ this.UpdateVisualState(false);
+ 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)
+ {
+ this.UpdateChannelSliderBackground(sender as ColorPickerSlider);
+ return;
+ }
+
+ ///
+ /// Event handler to draw checkered backgrounds on-demand as controls are loaded.
+ ///
+ private async void CheckeredBackgroundBorder_Loaded(object sender, RoutedEventArgs e)
+ {
+ await ColorPickerRenderingHelpers.UpdateBorderBackgroundWithCheckerAsync(
+ sender as 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, Windows.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 ColorRepToggleButton_CheckedUnchecked(object sender, RoutedEventArgs e)
+ {
+ if (object.ReferenceEquals(sender, this.HsvToggleButton))
+ {
+ this.SetActiveColorRepresentation(ColorRepresentation.Hsva);
+ }
+ else
+ {
+ this.SetActiveColorRepresentation(ColorRepresentation.Rgba);
+ }
+
+ this.UpdateColorControlValues();
+ this.UpdateChannelSliderBackgrounds();
+
+ 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, Windows.UI.Xaml.Input.PointerRoutedEventArgs e)
+ {
+ Border border = sender as Border;
+
+ if (border?.Background is SolidColorBrush brush)
+ {
+ this.ScheduleColorUpdate(brush.Color);
+ }
+
+ 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, Windows.UI.Xaml.Input.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 a key is pressed within a color channel TextBox.
+ /// This is used to trigger a re-evaluation of the color based on the TextBox value.
+ ///
+ private void ChannelTextBox_KeyDown(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs e)
+ {
+ if (e.Key == Windows.System.VirtualKey.Enter)
+ {
+ this.ApplyChannelTextBoxValue(sender as TextBox);
+ }
+
+ return;
+ }
+
+ ///
+ /// Event handler for when a color channel TextBox loses focus.
+ /// This is used to trigger a re-evaluation of the color based on the TextBox value.
+ ///
+ private void ChannelTextBox_LostFocus(object sender, RoutedEventArgs e)
+ {
+ this.ApplyChannelTextBoxValue(sender as TextBox);
+ 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/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPicker.xaml b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPicker.xaml
new file mode 100644
index 00000000000..3e32331d4d7
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPicker.xaml
@@ -0,0 +1,1341 @@
+
+
+
+
+ Transparent
+
+
+
+
+ #70F5F5F5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerButton.cs b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerButton.cs
new file mode 100644
index 00000000000..eb6b25f228b
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerButton.cs
@@ -0,0 +1,157 @@
+// 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;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Windows.UI;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ ///
+ /// A which displays a color as its Content and it's Flyout is a .
+ ///
+ [TemplatePart(Name = nameof(CheckeredBackgroundBorder), Type = typeof(Border))]
+ public 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("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("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));
+
+ #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.
+ ///
+ public ColorPickerButton()
+ {
+ this.DefaultStyleKey = typeof(ColorPickerButton);
+ }
+
+ ///
+ 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 = Windows.UI.Xaml.Controls.Primitives.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 void ColorPicker_ColorChanged(Windows.UI.Xaml.Controls.ColorPicker sender, ColorChangedEventArgs args)
+ {
+ SelectedColor = args.NewColor;
+ }
+
+ private async void CheckeredBackgroundBorder_Loaded(object sender, RoutedEventArgs e)
+ {
+ await ColorPickerRenderingHelpers.UpdateBorderBackgroundWithCheckerAsync(
+ sender as Border,
+ ColorPicker.CheckerBackgroundColor); // TODO: Check initialization
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerButton.xaml b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerButton.xaml
new file mode 100644
index 00000000000..a99f95dbc6d
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerButton.xaml
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerRenderingHelpers.cs b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerRenderingHelpers.cs
new file mode 100644
index 00000000000..8571cb54022
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerRenderingHelpers.cs
@@ -0,0 +1,531 @@
+// 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;
+using System.IO;
+using System.Runtime.InteropServices.WindowsRuntime;
+using System.Threading.Tasks;
+using Microsoft.Toolkit.Uwp.Helpers;
+using Windows.UI;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Media;
+using Windows.UI.Xaml.Media.Imaging;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ ///
+ /// Contains the rendering methods used within .
+ ///
+ internal class ColorPickerRenderingHelpers
+ {
+ ///
+ /// 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 = Uwp.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 = Uwp.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 = Uwp.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 = Uwp.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 = Uwp.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/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerSlider.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerSlider.Properties.cs
new file mode 100644
index 00000000000..65844e64aa7
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerSlider.Properties.cs
@@ -0,0 +1,241 @@
+// 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 Microsoft.Toolkit.Uwp.Helpers;
+using Windows.UI;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Media;
+
+namespace Microsoft.Toolkit.Uwp.UI.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(
+ new SolidColorBrush(Colors.Gray),
+ (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/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerSlider.cs b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerSlider.cs
new file mode 100644
index 00000000000..139b9a57553
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerSlider.cs
@@ -0,0 +1,301 @@
+// 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;
+using Microsoft.Toolkit.Uwp.Helpers;
+using Microsoft.Toolkit.Uwp.UI.Controls.ColorPickerConverters;
+using Windows.Foundation;
+using Windows.UI;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Media;
+
+namespace Microsoft.Toolkit.Uwp.UI.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 = Uwp.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 = Uwp.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();
+ this.Foreground = converter.Convert(selectedRgbColor, typeof(Brush), this.DefaultForeground, null) as Brush;
+
+ 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/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerSlider.xaml b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerSlider.xaml
new file mode 100644
index 00000000000..70d15c20190
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorPickerSlider.xaml
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorRepresentation.cs b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorRepresentation.cs
new file mode 100644
index 00000000000..39823709620
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorRepresentation.cs
@@ -0,0 +1,30 @@
+// 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;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.Toolkit.Uwp.UI.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/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorToColorShadeConverter.cs b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorToColorShadeConverter.cs
new file mode 100644
index 00000000000..a74e6e4ae54
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorToColorShadeConverter.cs
@@ -0,0 +1,125 @@
+// 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;
+using Microsoft.Toolkit.Uwp.Helpers;
+using Windows.UI;
+using Windows.UI.Xaml.Data;
+using Windows.UI.Xaml.Media;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls.ColorPickerConverters
+{
+ ///
+ /// Creates an accent color shade from a color value.
+ /// Only +/- 3 shades from the given color are supported.
+ ///
+ [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.")]
+ public class ColorToColorShadeConverter : IValueConverter
+ {
+ ///
+ public object Convert(
+ object value,
+ Type targetType,
+ object parameter,
+ string language)
+ {
+ int shade;
+ byte tolerance = 0x05;
+ double valueDelta = 0.25;
+ Color rgbColor;
+
+ // Get the current color in HSV
+ if (value is Color valueColor)
+ {
+ rgbColor = valueColor;
+ }
+ else if (value is SolidColorBrush valueBrush)
+ {
+ rgbColor = valueBrush.Color;
+ }
+ else
+ {
+ throw new ArgumentException("Invalid color value provided");
+ }
+
+ // Get the value component delta
+ try
+ {
+ shade = System.Convert.ToInt32(parameter?.ToString());
+ }
+ catch
+ {
+ throw new ArgumentException("Invalid parameter provided, unable to convert to integer");
+ }
+
+ // Specially handle minimum (black) and maximum (white)
+ if (rgbColor.R <= (0x00 + tolerance) &&
+ rgbColor.G <= (0x00 + tolerance) &&
+ rgbColor.B <= (0x00 + tolerance))
+ {
+ switch (shade)
+ {
+ case 1:
+ return Color.FromArgb(rgbColor.A, 0x3F, 0x3F, 0x3F);
+ case 2:
+ return Color.FromArgb(rgbColor.A, 0x80, 0x80, 0x80);
+ case 3:
+ return Color.FromArgb(rgbColor.A, 0xBF, 0xBF, 0xBF);
+ }
+
+ return rgbColor;
+ }
+ else if (rgbColor.R >= (0xFF + tolerance) &&
+ rgbColor.G >= (0xFF + tolerance) &&
+ rgbColor.B >= (0xFF + tolerance))
+ {
+ switch (shade)
+ {
+ case -1:
+ return Color.FromArgb(rgbColor.A, 0xBF, 0xBF, 0xBF);
+ case -2:
+ return Color.FromArgb(rgbColor.A, 0x80, 0x80, 0x80);
+ case -3:
+ return Color.FromArgb(rgbColor.A, 0x3F, 0x3F, 0x3F);
+ }
+
+ return rgbColor;
+ }
+ else
+ {
+ HsvColor hsvColor = rgbColor.ToHsv();
+
+ double colorHue = hsvColor.H;
+ double colorSaturation = hsvColor.S;
+ double colorValue = hsvColor.V;
+ double colorAlpha = hsvColor.A;
+
+ // Use the HSV representation as it's more perceptual.
+ // Only the value is changed by a fixed percentage so the algorithm is reproducible.
+ // This does not account for perceptual differences and also does not match with
+ // system accent color calculation.
+ if (shade != 0)
+ {
+ colorValue *= 1.0 + (shade * valueDelta);
+ }
+
+ return Uwp.Helpers.ColorHelper.FromHsv(
+ Math.Clamp(colorHue, 0.0, 360.0),
+ Math.Clamp(colorSaturation, 0.0, 1.0),
+ Math.Clamp(colorValue, 0.0, 1.0),
+ Math.Clamp(colorAlpha, 0.0, 1.0));
+ }
+ }
+
+ ///
+ public object ConvertBack(
+ object value,
+ Type targetType,
+ object parameter,
+ string language)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorToHexConverter.cs b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorToHexConverter.cs
new file mode 100644
index 00000000000..b460c8820fa
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ColorToHexConverter.cs
@@ -0,0 +1,77 @@
+// 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;
+using Microsoft.Toolkit.Uwp.Helpers;
+using Windows.UI;
+using Windows.UI.Xaml.Data;
+using Windows.UI.Xaml.Media;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls.ColorPickerConverters
+{
+ ///
+ /// 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
+ {
+ throw new ArgumentException("Invalid color value provided");
+ }
+
+ 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
+ {
+ throw new ArgumentException("Invalid hex color value provided");
+ }
+ }
+ else
+ {
+ try
+ {
+ return ("#" + hexValue).ToColor();
+ }
+ catch
+ {
+ throw new ArgumentException("Invalid hex color value provided");
+ }
+ }
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ContrastBrushConverter.cs b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ContrastBrushConverter.cs
new file mode 100644
index 00000000000..abd1dc1dbd7
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/ContrastBrushConverter.cs
@@ -0,0 +1,119 @@
+// 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;
+using Windows.UI;
+using Windows.UI.Xaml.Data;
+using Windows.UI.Xaml.Media;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls.ColorPickerConverters
+{
+ ///
+ /// 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
+ {
+ throw new ArgumentException("Invalid color value provided");
+ }
+
+ // 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)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// 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/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/FluentColorPalette.cs b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/FluentColorPalette.cs
new file mode 100644
index 00000000000..25b848ecf18
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/FluentColorPalette.cs
@@ -0,0 +1,175 @@
+// 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;
+using Color = Windows.UI.Color; // Type can be changed to CoreColor, etc.
+
+namespace Microsoft.Toolkit.Uwp.UI.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, 67, 67), /* #FF4343 */
+ Color.FromArgb(255, 209, 52, 56), /* #D13438 */
+ Color.FromArgb(255, 239, 105, 80), /* #EF6950 */
+ Color.FromArgb(255, 218, 59, 1), /* #DA3B01 */
+ Color.FromArgb(255, 202, 80, 16), /* #CA5010 */
+ Color.FromArgb(255, 247, 99, 12), /* #F7630C */
+ Color.FromArgb(255, 255, 140, 0), /* #FF8C00 */
+ Color.FromArgb(255, 255, 185, 0), /* #FFB900 */
+ },
+ {
+ Color.FromArgb(255, 231, 72, 86), /* #E74856 */
+ Color.FromArgb(255, 232, 17, 35), /* #E81123 */
+ Color.FromArgb(255, 234, 0, 94), /* #EA005E */
+ Color.FromArgb(255, 195, 0, 82), /* #C30052 */
+ Color.FromArgb(255, 227, 0, 140), /* #E3008C */
+ Color.FromArgb(255, 191, 0, 119), /* #BF0077 */
+ Color.FromArgb(255, 194, 57, 179), /* #C239B3 */
+ Color.FromArgb(255, 154, 0, 137), /* #9A0089 */
+ },
+ {
+ Color.FromArgb(255, 0, 120, 215), /* #0078D7 */
+ Color.FromArgb(255, 0, 99, 177), /* #0063B1 */
+ Color.FromArgb(255, 142, 140, 216), /* #8E8CD8 */
+ Color.FromArgb(255, 107, 105, 214), /* #6B69D6 */
+ Color.FromArgb(255, 135, 100, 184), /* #8764B8 */
+ Color.FromArgb(255, 116, 77, 169), /* #744DA9 */
+ Color.FromArgb(255, 177, 70, 194), /* #B146C2 */
+ Color.FromArgb(255, 136, 23, 152), /* #881798 */
+ },
+ {
+ Color.FromArgb(255, 0, 153, 188), /* #0099BC */
+ Color.FromArgb(255, 45, 125, 154), /* #2D7D9A */
+ Color.FromArgb(255, 0, 183, 195), /* #00B7C3 */
+ Color.FromArgb(255, 3, 131, 135), /* #038387 */
+ Color.FromArgb(255, 0, 178, 148), /* #00B294 */
+ Color.FromArgb(255, 1, 133, 116), /* #018574 */
+ Color.FromArgb(255, 0, 204, 106), /* #00CC6A */
+ Color.FromArgb(255, 16, 137, 62), /* #10893E */
+ },
+ {
+ Color.FromArgb(255, 122, 117, 116), /* #7A7574 */
+ Color.FromArgb(255, 93, 90, 80), /* #5D5A58 */
+ Color.FromArgb(255, 104, 118, 138), /* #68768A */
+ Color.FromArgb(255, 81, 92, 107), /* #515C6B */
+ Color.FromArgb(255, 86, 124, 115), /* #567C73 */
+ Color.FromArgb(255, 72, 104, 96), /* #486860 */
+ Color.FromArgb(255, 73, 130, 5), /* #498205 */
+ Color.FromArgb(255, 16, 124, 16), /* #107C10 */
+ },
+ {
+ Color.FromArgb(255, 118, 118, 118), /* #767676 */
+ Color.FromArgb(255, 76, 74, 72), /* #4C4A48 */
+ Color.FromArgb(255, 105, 121, 126), /* #69797E */
+ Color.FromArgb(255, 74, 84, 89), /* #4A5459 */
+ Color.FromArgb(255, 100, 124, 100), /* #647C64 */
+ Color.FromArgb(255, 82, 94, 84), /* #525E54 */
+ Color.FromArgb(255, 132, 117, 69), /* #847545 */
+ Color.FromArgb(255, 126, 115, 95), /* #7E735F */
+ }
+ };
+
+ /***************************************************************************************
+ *
+ * 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/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/IColorPalette.cs b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/IColorPalette.cs
new file mode 100644
index 00000000000..f260bf8be6e
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/ColorPicker/IColorPalette.cs
@@ -0,0 +1,36 @@
+// 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 Microsoft.Toolkit.Uwp.UI.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/Microsoft.Toolkit.Uwp.UI.Controls/InfiniteCanvas/InfiniteCanvas.TextBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls/InfiniteCanvas/InfiniteCanvas.TextBox.cs
index 94a537fbb53..6edfd098f16 100644
--- a/Microsoft.Toolkit.Uwp.UI.Controls/InfiniteCanvas/InfiniteCanvas.TextBox.cs
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/InfiniteCanvas/InfiniteCanvas.TextBox.cs
@@ -108,7 +108,7 @@ private void CanvasTextBox_SizeChanged(object sender, SizeChangedEventArgs e)
SelectedTextDrawable?.UpdateBounds(_canvasTextBox.ActualWidth, _canvasTextBox.ActualHeight);
}
- private void CanvasTextBoxColorPicker_ColorChanged(ColorPicker sender, ColorChangedEventArgs args)
+ private void CanvasTextBoxColorPicker_ColorChanged(Windows.UI.Xaml.Controls.ColorPicker sender, ColorChangedEventArgs args)
{
if (SelectedTextDrawable != null)
{
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/InfiniteCanvas/InfiniteCanvas.cs b/Microsoft.Toolkit.Uwp.UI.Controls/InfiniteCanvas/InfiniteCanvas.cs
index 23cb0a3ef3c..be4e955b281 100644
--- a/Microsoft.Toolkit.Uwp.UI.Controls/InfiniteCanvas/InfiniteCanvas.cs
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/InfiniteCanvas/InfiniteCanvas.cs
@@ -20,7 +20,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
/// InfiniteCanvas is a canvas that supports Ink, Text, Format Text, Zoom in/out, Redo, Undo, Export canvas data, Import canvas data.
///
[TemplatePart(Name = CanvasTextBoxToolsName, Type = typeof(StackPanel))]
- [TemplatePart(Name = CanvasTextBoxColorPickerName, Type = typeof(ColorPicker))]
+ [TemplatePart(Name = CanvasTextBoxColorPickerName, Type = typeof(Windows.UI.Xaml.Controls.ColorPicker))]
[TemplatePart(Name = CanvasTextBoxFontSizeTextBoxName, Type = typeof(TextBox))]
[TemplatePart(Name = CanvasTextBoxItalicButtonName, Type = typeof(ToggleButton))]
[TemplatePart(Name = CanvasTextBoxBoldButtonName, Type = typeof(ToggleButton))]
@@ -69,7 +69,7 @@ public partial class InfiniteCanvas : Control
private InkToolbarCustomToggleButton _enableTouchInkingButton;
private InfiniteCanvasTextBox _canvasTextBox;
private StackPanel _canvasTextBoxTools;
- private ColorPicker _canvasTextBoxColorPicker;
+ private Windows.UI.Xaml.Controls.ColorPicker _canvasTextBoxColorPicker;
private TextBox _canvasTextBoxFontSizeTextBox;
private ToggleButton _canvasTextBoxItalicButton;
@@ -243,7 +243,7 @@ public InfiniteCanvas()
protected override void OnApplyTemplate()
{
_canvasTextBoxTools = (StackPanel)GetTemplateChild(CanvasTextBoxToolsName);
- _canvasTextBoxColorPicker = (ColorPicker)GetTemplateChild(CanvasTextBoxColorPickerName);
+ this._canvasTextBoxColorPicker = (Windows.UI.Xaml.Controls.ColorPicker)GetTemplateChild(CanvasTextBoxColorPickerName);
_canvasTextBoxFontSizeTextBox = (TextBox)GetTemplateChild(CanvasTextBoxFontSizeTextBoxName);
_canvasTextBoxItalicButton = (ToggleButton)GetTemplateChild(CanvasTextBoxItalicButtonName);
_canvasTextBoxBoldButton = (ToggleButton)GetTemplateChild(CanvasTextBoxBoldButtonName);
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/InfiniteCanvas/InfiniteCanvas.xaml b/Microsoft.Toolkit.Uwp.UI.Controls/InfiniteCanvas/InfiniteCanvas.xaml
index 9d78d87c390..975ef857783 100644
--- a/Microsoft.Toolkit.Uwp.UI.Controls/InfiniteCanvas/InfiniteCanvas.xaml
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/InfiniteCanvas/InfiniteCanvas.xaml
@@ -63,6 +63,7 @@
+
-
-
-
- MSBuild:Compile
-
-
- MSBuild:Compile
-
-
-
-
-
- Designer
-
-
- MSBuild:Compile
-
-
- MSBuild:Compile
-
-
-
-
-
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/SwitchPresenter/Case.cs b/Microsoft.Toolkit.Uwp.UI.Controls/SwitchPresenter/Case.cs
new file mode 100644
index 00000000000..b0ebd72361f
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/SwitchPresenter/Case.cs
@@ -0,0 +1,83 @@
+// 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;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Markup;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ ///
+ /// is the value container for the .
+ ///
+ [ContentProperty(Name = nameof(Content))]
+ public class Case : DependencyObject
+ {
+ internal SwitchPresenter Parent { get; set; } // TODO: Can we remove Parent need here and just use events?
+
+ ///
+ /// Event raised when the property changes.
+ ///
+ public event EventHandler ValueChanged;
+
+ ///
+ /// Gets or sets the Content to display when this case is active.
+ ///
+ public UIElement Content
+ {
+ get { return (UIElement)GetValue(ContentProperty); }
+ set { SetValue(ContentProperty, value); }
+ }
+
+ ///
+ /// Identifies the property.
+ ///
+ public static readonly DependencyProperty ContentProperty =
+ DependencyProperty.Register(nameof(Content), typeof(UIElement), typeof(Case), new PropertyMetadata(null));
+
+ ///
+ /// Gets or sets a value indicating whether this is the default case to display when no values match the specified value in the . There should only be a single default case provided. Do not set the property when setting to true. Default is false.
+ ///
+ public bool IsDefault
+ {
+ get { return (bool)GetValue(IsDefaultProperty); }
+ set { SetValue(IsDefaultProperty, value); }
+ }
+
+ ///
+ /// Identifies the property.
+ ///
+ public static readonly DependencyProperty IsDefaultProperty =
+ DependencyProperty.Register(nameof(IsDefault), typeof(bool), typeof(Case), new PropertyMetadata(false));
+
+ ///
+ /// Gets or sets the that this case represents. If it matches the this case's will be displayed in the presenter.
+ ///
+ public object Value
+ {
+ get { return (object)GetValue(ValueProperty); }
+ set { SetValue(ValueProperty, value); }
+ }
+
+ ///
+ /// Identifies the property.
+ ///
+ public static readonly DependencyProperty ValueProperty =
+ DependencyProperty.Register(nameof(Value), typeof(object), typeof(Case), new PropertyMetadata(null, OnValuePropertyChanged));
+
+ private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var xcase = (Case)d;
+
+ xcase.ValueChanged?.Invoke(xcase, EventArgs.Empty);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Case()
+ {
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/SwitchPresenter/CaseCollection.cs b/Microsoft.Toolkit.Uwp.UI.Controls/SwitchPresenter/CaseCollection.cs
new file mode 100644
index 00000000000..d1f3ee96187
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/SwitchPresenter/CaseCollection.cs
@@ -0,0 +1,134 @@
+// 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;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ ///
+ /// An collection of to help with XAML interop.
+ ///
+ public class CaseCollection : IList, IEnumerable // TODO: Do we need this or can we use an ObservableCollection directly??? (Or is it useful to have it manage the registration of the child events?)
+ {
+ internal SwitchPresenter Parent { get; set; } // TODO: Can we remove Parent need here and just use events?
+
+ private readonly List _internalList = new List();
+
+ ///
+ public int Count => _internalList.Count;
+
+ ///
+ public bool IsReadOnly => false;
+
+ ///
+ public Case this[int index] { get => _internalList[index]; set => Insert(index, value); }
+
+ ///
+ /// Raised when an animation has been added/removed or modified
+ ///
+ public event EventHandler CaseCollectionChanged;
+
+ private void ValueChanged(object sender, EventArgs e)
+ {
+ CaseCollectionChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CaseCollection()
+ {
+ }
+
+ ///
+ public int IndexOf(Case item)
+ {
+ return _internalList.IndexOf(item);
+ }
+
+ ///
+ public void Insert(int index, Case item)
+ {
+ item.ValueChanged += ValueChanged;
+ item.Parent = Parent;
+ _internalList.Insert(index, item);
+ CaseCollectionChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ public void RemoveAt(int index)
+ {
+ if (index >= 0 && index < _internalList.Count)
+ {
+ var xcase = _internalList[index];
+ xcase.ValueChanged -= ValueChanged;
+ xcase.Parent = null;
+ }
+
+ _internalList.RemoveAt(index);
+ CaseCollectionChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ public void Add(Case item)
+ {
+ item.ValueChanged += ValueChanged;
+ item.Parent = Parent;
+ _internalList.Add(item);
+ CaseCollectionChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ public void Clear()
+ {
+ foreach (var xcase in _internalList)
+ {
+ xcase.ValueChanged -= ValueChanged;
+ xcase.Parent = null;
+ }
+
+ _internalList.Clear();
+ CaseCollectionChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ public bool Contains(Case item)
+ {
+ return _internalList.Contains(item);
+ }
+
+ ///
+ public void CopyTo(Case[] array, int arrayIndex)
+ {
+ _internalList.CopyTo(array, arrayIndex);
+ }
+
+ ///
+ public bool Remove(Case item)
+ {
+ var result = _internalList.Remove(item);
+ if (result)
+ {
+ item.ValueChanged -= ValueChanged;
+ item.Parent = null;
+ CaseCollectionChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ return result;
+ }
+
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ return _internalList.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _internalList.GetEnumerator();
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/SwitchPresenter/SwitchPresenter.cs b/Microsoft.Toolkit.Uwp.UI.Controls/SwitchPresenter/SwitchPresenter.cs
new file mode 100644
index 00000000000..cbe6f1f3fcd
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/SwitchPresenter/SwitchPresenter.cs
@@ -0,0 +1,246 @@
+// 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;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Markup;
+using Windows.UI.Xaml.Media;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ ///
+ /// The is a which can allow a developer to mimic a switch statement within XAML.
+ /// When provided a set of s and a , it will pick the matching with the corresponding .
+ ///
+ [ContentProperty(Name = nameof(SwitchCases))]
+ public sealed class SwitchPresenter : ContentPresenter
+ {
+ ///
+ /// Gets the current which is being displayed.
+ ///
+ public Case CurrentCase
+ {
+ get { return (Case)GetValue(CurrentCaseProperty); }
+ private set { SetValue(CurrentCaseProperty, value); }
+ }
+
+ ///
+ /// Indicates the property.
+ ///
+ public static readonly DependencyProperty CurrentCaseProperty =
+ DependencyProperty.Register(nameof(CurrentCase), typeof(Case), typeof(SwitchPresenter), new PropertyMetadata(null));
+
+ ///
+ /// Gets or sets a value representing the collection of cases to evaluate.
+ ///
+ public CaseCollection SwitchCases
+ {
+ get { return (CaseCollection)GetValue(SwitchCasesProperty); }
+ set { SetValue(SwitchCasesProperty, value); }
+ }
+
+ ///
+ /// Indicates the property.
+ ///
+ public static readonly DependencyProperty SwitchCasesProperty =
+ DependencyProperty.Register(nameof(SwitchCases), typeof(CaseCollection), typeof(SwitchPresenter), new PropertyMetadata(null, new PropertyChangedCallback(OnSwitchCasesPropertyChanged)));
+
+ ///
+ /// Gets or sets a value indicating the value to compare all cases against. When this value is bound to and changes, the presenter will automatically evaluate cases and select the new appropriate content from the switch.
+ ///
+ public object Value
+ {
+ get { return (object)GetValue(ValueProperty); }
+ set { SetValue(ValueProperty, value); }
+ }
+
+ ///
+ /// Indicates the property.
+ ///
+ public static readonly DependencyProperty ValueProperty =
+ DependencyProperty.Register(nameof(Value), typeof(object), typeof(SwitchPresenter), new PropertyMetadata(null, new PropertyChangedCallback(OnValuePropertyChanged)));
+
+ ///
+ /// Gets or sets a value indicating which type to first cast and compare provided values against.
+ ///
+ public Type TargetType
+ {
+ get { return (Type)GetValue(DataTypeProperty); }
+ set { SetValue(DataTypeProperty, value); }
+ }
+
+ ///
+ /// Indicates the property.
+ ///
+ public static readonly DependencyProperty DataTypeProperty =
+ DependencyProperty.Register(nameof(TargetType), typeof(Type), typeof(SwitchPresenter), new PropertyMetadata(null));
+
+ ///
+ /// Gets or sets a value indicating whether the content is removed from the visual tree when switching between cases.
+ ///
+ public bool IsVisualTreeDisconnectedOnChange { get; set; }
+
+ private static void OnSwitchCasesPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (e.OldValue != null)
+ {
+ ((SwitchPresenter)e.OldValue).SwitchCases.CaseCollectionChanged -= OnCaseValuePropertyChanged;
+ }
+
+ var xswitch = (SwitchPresenter)d;
+
+ foreach (var xcase in xswitch.SwitchCases)
+ {
+ // Set our parent
+ xcase.Parent = xswitch;
+ }
+
+ // Will trigger on collection change and case value changed
+ xswitch.SwitchCases.Parent = xswitch;
+ xswitch.SwitchCases.CaseCollectionChanged += OnCaseValuePropertyChanged;
+ }
+
+ private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ // When our Switch's expression changes, re-evaluate.
+ var xswitch = (SwitchPresenter)d;
+
+ xswitch.EvaluateCases();
+ }
+
+ private static void OnCaseValuePropertyChanged(object sender, EventArgs e)
+ {
+ // When something about our collection of cases changes, re-evaluate.
+ var collection = (CaseCollection)sender;
+
+ collection.Parent.EvaluateCases();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SwitchPresenter()
+ {
+ this.SwitchCases = new CaseCollection();
+ }
+
+ ///
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ EvaluateCases();
+ }
+
+ private void EvaluateCases()
+ {
+ if (CurrentCase != null &&
+ CurrentCase.Value != null &&
+ CurrentCase.Value.Equals(Value))
+ {
+ // If the current case we're on already matches our current value,
+ // then we don't have any work to do.
+ return;
+ }
+
+ Case xdefault = null;
+ Case newcase = null;
+
+ foreach (var xcase in SwitchCases)
+ {
+ if (xcase.IsDefault)
+ {
+ // If there are multiple default cases provided, this will override and just grab the last one, the developer will have to fix this in their XAML. We call this out in the case comments.
+ xdefault = xcase;
+ continue;
+ }
+
+ if (CompareValues(Value, xcase.Value))
+ {
+ newcase = xcase;
+ break;
+ }
+ }
+
+ if (newcase == null && xdefault != null)
+ {
+ // Inject default if we found one.
+ newcase = xdefault;
+ }
+
+ // Only bother changing things around if we have a new case.
+ if (newcase != CurrentCase)
+ {
+ // Disconnect old content from visual tree.
+ if (CurrentCase != null && CurrentCase.Content != null && IsVisualTreeDisconnectedOnChange)
+ {
+ // TODO: If we disconnect here, we need to recreate later??? Need to Test...
+ VisualTreeHelper.DisconnectChildrenRecursive(CurrentCase.Content);
+ }
+
+ // Hookup new content.
+ Content = newcase.Content;
+
+ CurrentCase = newcase;
+ }
+ }
+
+ ///
+ /// Compares two values using the TargetType.
+ ///
+ /// Our main value in our SwitchPresenter.
+ /// The value from the case to compare to.
+ /// true if the two values are equal
+ private bool CompareValues(object compare, object value)
+ {
+ if (compare == null || value == null)
+ {
+ return compare == value;
+ }
+
+ if (TargetType == null ||
+ (TargetType == compare.GetType() &&
+ TargetType == value.GetType()))
+ {
+ // Default direct object comparison or we're all the proper type
+ return compare.Equals(value);
+ }
+ else if (compare.GetType() == TargetType)
+ {
+ // If we have a TargetType and the first value is ther right type
+ // Then our 2nd value isn't, so convert to string and coerce.
+ var valueBase2 = ConvertValue(TargetType, value);
+
+ return compare.Equals(valueBase2);
+ }
+
+ // Neither of our two values matches the type so
+ // we'll convert both to a String and try and coerce it to the proper type.
+ var compareBase = ConvertValue(TargetType, compare);
+
+ var valueBase = ConvertValue(TargetType, value);
+
+ return compareBase.Equals(valueBase);
+ }
+
+ ///
+ /// Helper method to convert a value from a source type to a target type.
+ ///
+ /// The target type
+ /// The value to convert
+ /// The converted value
+ internal static object ConvertValue(Type targetType, object value)
+ {
+ if (targetType.IsInstanceOfType(value))
+ {
+ return value;
+ }
+ else
+ {
+ return XamlBindingHelper.ConvertValue(targetType, value);
+ }
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/Themes/Generic.xaml b/Microsoft.Toolkit.Uwp.UI.Controls/Themes/Generic.xaml
index 8354174809c..6422f8690d2 100644
--- a/Microsoft.Toolkit.Uwp.UI.Controls/Themes/Generic.xaml
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/Themes/Generic.xaml
@@ -4,6 +4,9 @@
+
+
+
diff --git a/Microsoft.Toolkit.Uwp.UI/Converters/ColorToDisplayNameConverter.cs b/Microsoft.Toolkit.Uwp.UI/Converters/ColorToDisplayNameConverter.cs
new file mode 100644
index 00000000000..5aa9a78cefe
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI/Converters/ColorToDisplayNameConverter.cs
@@ -0,0 +1,52 @@
+// 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;
+using Windows.UI;
+using Windows.UI.Xaml.Data;
+using Windows.UI.Xaml.Media;
+
+namespace Microsoft.Toolkit.Uwp.UI.Converters
+{
+ ///
+ /// Gets the approximated display name for the color.
+ ///
+ public class ColorToDisplayNameConverter : 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
+ {
+ throw new ArgumentException("Invalid color value provided");
+ }
+
+ return ColorHelper.ToDisplayName(color);
+ }
+
+ ///
+ public object ConvertBack(
+ object value,
+ Type targetType,
+ object parameter,
+ string language)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Windows Community Toolkit.sln b/Windows Community Toolkit.sln
index dbb8ef27886..8a84a30043d 100644
--- a/Windows Community Toolkit.sln
+++ b/Windows Community Toolkit.sln
@@ -57,6 +57,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
global.json = global.json
+ settings.xamlstyler = settings.xamlstyler
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Toolkit.Uwp.UI.Controls.Design", "Microsoft.Toolkit.Uwp.UI.Controls.Design\Microsoft.Toolkit.Uwp.UI.Controls.Design.csproj", "{7AEFC959-ED7C-4D96-9E92-72609B40FBE0}"
@@ -569,34 +570,6 @@ Global
{42CA4935-54BE-42EA-AC19-992378C08DE6}.Release|x64.Build.0 = Release|Any CPU
{42CA4935-54BE-42EA-AC19-992378C08DE6}.Release|x86.ActiveCfg = Release|Any CPU
{42CA4935-54BE-42EA-AC19-992378C08DE6}.Release|x86.Build.0 = Release|Any CPU
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Debug|Any CPU.ActiveCfg = Debug|Win32
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Debug|Any CPU.Build.0 = Debug|Win32
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Debug|ARM.ActiveCfg = Debug|ARM
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Debug|ARM.Build.0 = Debug|ARM
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Debug|ARM64.ActiveCfg = Debug|ARM64
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Debug|ARM64.Build.0 = Debug|ARM64
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Debug|x64.ActiveCfg = Debug|x64
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Debug|x64.Build.0 = Debug|x64
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Debug|x86.ActiveCfg = Debug|Win32
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Debug|x86.Build.0 = Debug|Win32
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Native|Any CPU.ActiveCfg = Release|Win32
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Native|ARM.ActiveCfg = Release|ARM
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Native|ARM.Build.0 = Release|ARM
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Native|ARM64.ActiveCfg = Release|ARM64
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Native|ARM64.Build.0 = Release|ARM64
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Native|x64.ActiveCfg = Release|x64
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Native|x64.Build.0 = Release|x64
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Native|x86.ActiveCfg = Release|Win32
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Native|x86.Build.0 = Release|Win32
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Release|Any CPU.ActiveCfg = Release|Win32
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Release|ARM.ActiveCfg = Release|ARM
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Release|ARM.Build.0 = Release|ARM
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Release|ARM64.ActiveCfg = Release|ARM64
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Release|ARM64.Build.0 = Release|ARM64
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Release|x64.ActiveCfg = Release|x64
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Release|x64.Build.0 = Release|x64
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Release|x86.ActiveCfg = Release|Win32
- {A5E98964-45B1-442D-A07A-298A3221D81E}.Release|x86.Build.0 = Release|Win32
{5BF75694-798A-43A0-8150-415DE195359C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5BF75694-798A-43A0-8150-415DE195359C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5BF75694-798A-43A0-8150-415DE195359C}.Debug|ARM.ActiveCfg = Debug|Any CPU