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 @@ Designer MSBuild:Compile + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + MSBuild:Compile Designer 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 @@ +