From 215833c1a38668a09b0b88570544839c964b2f54 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Wed, 20 Sep 2023 11:55:15 +0200 Subject: [PATCH 01/18] Init --- components/ColorPicker/OpenSolution.bat | 3 + .../ColorPicker/samples/Assets/icon.png | Bin 0 -> 6192 bytes .../samples/ColorPicker.Samples.csproj | 8 + components/ColorPicker/samples/ColorPicker.md | 47 + .../samples/ColorPickerCustomSample.xaml | 19 + .../samples/ColorPickerCustomSample.xaml.cs | 30 + .../ColorPicker/samples/Dependencies.props | 31 + components/ColorPicker/src/ColorChannel.cs | 32 + .../ColorPicker/src/ColorPicker.Properties.cs | 143 ++ components/ColorPicker/src/ColorPicker.cs | 1489 +++++++++++++++++ components/ColorPicker/src/ColorPicker.xaml | 1251 ++++++++++++++ .../ColorPicker/src/ColorPickerButton.cs | 161 ++ .../ColorPicker/src/ColorPickerButton.xaml | 163 ++ .../src/ColorPickerRenderingHelpers.cs | 536 ++++++ .../src/ColorPickerSlider.Properties.cs | 242 +++ .../ColorPicker/src/ColorPickerSlider.cs | 294 ++++ .../ColorPicker/src/ColorPickerSlider.xaml | 175 ++ .../src/ColorPreviewer.Properties.cs | 73 + components/ColorPicker/src/ColorPreviewer.cs | 231 +++ .../ColorPicker/src/ColorPreviewer.xaml | 122 ++ .../ColorPicker/src/ColorRepresentation.cs | 22 + ...yToolkit.WinUI.Controls.ColorPicker.csproj | 19 + .../src/Converters/AccentColorConverter.cs | 122 ++ .../src/Converters/ColorToHexConverter.cs | 76 + .../src/Converters/ContrastBrushConverter.cs | 122 ++ components/ColorPicker/src/Dependencies.props | 31 + .../ColorPicker/src/FluentColorPalette.cs | 173 ++ components/ColorPicker/src/IColorPalette.cs | 35 + components/ColorPicker/src/MultiTarget.props | 9 + .../ColorPicker/src/Themes/Generic.xaml | 12 + .../tests/ColorPicker.Tests.projitems | 23 + .../tests/ColorPicker.Tests.shproj | 13 + .../tests/ExampleColorPickerTestClass.cs | 135 ++ .../tests/ExampleColorPickerTestPage.xaml | 14 + .../tests/ExampleColorPickerTestPage.xaml.cs | 16 + 35 files changed, 5872 insertions(+) create mode 100644 components/ColorPicker/OpenSolution.bat create mode 100644 components/ColorPicker/samples/Assets/icon.png create mode 100644 components/ColorPicker/samples/ColorPicker.Samples.csproj create mode 100644 components/ColorPicker/samples/ColorPicker.md create mode 100644 components/ColorPicker/samples/ColorPickerCustomSample.xaml create mode 100644 components/ColorPicker/samples/ColorPickerCustomSample.xaml.cs create mode 100644 components/ColorPicker/samples/Dependencies.props create mode 100644 components/ColorPicker/src/ColorChannel.cs create mode 100644 components/ColorPicker/src/ColorPicker.Properties.cs create mode 100644 components/ColorPicker/src/ColorPicker.cs create mode 100644 components/ColorPicker/src/ColorPicker.xaml create mode 100644 components/ColorPicker/src/ColorPickerButton.cs create mode 100644 components/ColorPicker/src/ColorPickerButton.xaml create mode 100644 components/ColorPicker/src/ColorPickerRenderingHelpers.cs create mode 100644 components/ColorPicker/src/ColorPickerSlider.Properties.cs create mode 100644 components/ColorPicker/src/ColorPickerSlider.cs create mode 100644 components/ColorPicker/src/ColorPickerSlider.xaml create mode 100644 components/ColorPicker/src/ColorPreviewer.Properties.cs create mode 100644 components/ColorPicker/src/ColorPreviewer.cs create mode 100644 components/ColorPicker/src/ColorPreviewer.xaml create mode 100644 components/ColorPicker/src/ColorRepresentation.cs create mode 100644 components/ColorPicker/src/CommunityToolkit.WinUI.Controls.ColorPicker.csproj create mode 100644 components/ColorPicker/src/Converters/AccentColorConverter.cs create mode 100644 components/ColorPicker/src/Converters/ColorToHexConverter.cs create mode 100644 components/ColorPicker/src/Converters/ContrastBrushConverter.cs create mode 100644 components/ColorPicker/src/Dependencies.props create mode 100644 components/ColorPicker/src/FluentColorPalette.cs create mode 100644 components/ColorPicker/src/IColorPalette.cs create mode 100644 components/ColorPicker/src/MultiTarget.props create mode 100644 components/ColorPicker/src/Themes/Generic.xaml create mode 100644 components/ColorPicker/tests/ColorPicker.Tests.projitems create mode 100644 components/ColorPicker/tests/ColorPicker.Tests.shproj create mode 100644 components/ColorPicker/tests/ExampleColorPickerTestClass.cs create mode 100644 components/ColorPicker/tests/ExampleColorPickerTestPage.xaml create mode 100644 components/ColorPicker/tests/ExampleColorPickerTestPage.xaml.cs diff --git a/components/ColorPicker/OpenSolution.bat b/components/ColorPicker/OpenSolution.bat new file mode 100644 index 00000000..814a56d4 --- /dev/null +++ b/components/ColorPicker/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/ColorPicker/samples/Assets/icon.png b/components/ColorPicker/samples/Assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5f574ceca233597ecc5d775fd2f516c0d9685cfa GIT binary patch literal 6192 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 O0000 + + ColorPicker + + + + + diff --git a/components/ColorPicker/samples/ColorPicker.md b/components/ColorPicker/samples/ColorPicker.md new file mode 100644 index 00000000..77cf850b --- /dev/null +++ b/components/ColorPicker/samples/ColorPicker.md @@ -0,0 +1,47 @@ +--- +title: ColorPicker +author: githubaccount +description: TODO: Your experiment's description here +keywords: ColorPicker, Control, Layout +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 0 +issue-id: 0 +icon: assets/icon.png +--- + + + + + + + + + +# ColorPicker + +TODO: Fill in information about this experiment and how to get started here... + +## Custom Control + +You can inherit from an existing component as well, like `Panel`, this example shows a control without a +XAML Style that will be more light-weight to consume by an app developer: + +> [!Sample ColorPickerCustomSample] + +## Templated Controls + +The Toolkit is built with templated controls. This provides developers a flexible way to restyle components +easily while still inheriting the general functionality a control provides. The examples below show +how a component can use a default style and then get overridden by the end developer. + +TODO: Two types of templated control building methods are shown. Delete these if you're building a custom component. +Otherwise, pick one method for your component and delete the files related to the unchosen `_ClassicBinding` or `_xBind` +classes (and the custom non-suffixed one as well). Then, rename your component to just be your component name. + +The `_ClassicBinding` class shows the traditional method used to develop components with best practices. + + + diff --git a/components/ColorPicker/samples/ColorPickerCustomSample.xaml b/components/ColorPicker/samples/ColorPickerCustomSample.xaml new file mode 100644 index 00000000..3f873cd7 --- /dev/null +++ b/components/ColorPicker/samples/ColorPickerCustomSample.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/components/ColorPicker/samples/ColorPickerCustomSample.xaml.cs b/components/ColorPicker/samples/ColorPickerCustomSample.xaml.cs new file mode 100644 index 00000000..81c45e2f --- /dev/null +++ b/components/ColorPicker/samples/ColorPickerCustomSample.xaml.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 CommunityToolkit.WinUI.Controls; + +namespace ColorPickerExperiment.Samples; + +/// +/// An example sample page of a custom control inheriting from Panel. +/// +[ToolkitSampleTextOption("TitleText", "This is a title", Title = "Input the text")] +[ToolkitSampleMultiChoiceOption("LayoutOrientation", "Horizontal", "Vertical", Title = "Orientation")] + +[ToolkitSample(id: nameof(ColorPickerCustomSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(CommunityToolkit.WinUI.Controls.ColorPicker)} custom control.")] +public sealed partial class ColorPickerCustomSample : Page +{ + public ColorPickerCustomSample() + { + this.InitializeComponent(); + } + + // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149 + public static Orientation ConvertStringToOrientation(string orientation) => orientation switch + { + "Vertical" => Orientation.Vertical, + "Horizontal" => Orientation.Horizontal, + _ => throw new System.NotImplementedException(), + }; +} diff --git a/components/ColorPicker/samples/Dependencies.props b/components/ColorPicker/samples/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/ColorPicker/samples/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/ColorPicker/src/ColorChannel.cs b/components/ColorPicker/src/ColorChannel.cs new file mode 100644 index 00000000..25a7e6c5 --- /dev/null +++ b/components/ColorPicker/src/ColorChannel.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Defines a specific channel within a color representation. +/// +[EditorBrowsable(EditorBrowsableState.Advanced)] +public enum ColorChannel +{ + /// + /// Represents the alpha channel. + /// + Alpha, + + /// + /// Represents the first color channel which is Red when RGB or Hue when HSV. + /// + Channel1, + + /// + /// Represents the second color channel which is Green when RGB or Saturation when HSV. + /// + Channel2, + + /// + /// Represents the third color channel which is Blue when RGB or Value when HSV. + /// + Channel3 +} diff --git a/components/ColorPicker/src/ColorPicker.Properties.cs b/components/ColorPicker/src/ColorPicker.Properties.cs new file mode 100644 index 00000000..509aef54 --- /dev/null +++ b/components/ColorPicker/src/ColorPicker.Properties.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Contains all properties for the . +/// +public partial class ColorPicker +{ + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty CustomPaletteColorsProperty = + DependencyProperty.Register( + nameof(CustomPaletteColors), + typeof(ObservableCollection), + typeof(ColorPicker), + new PropertyMetadata( + null, + (s, e) => (s as ColorPicker)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets the list of custom palette colors. + /// + public ObservableCollection CustomPaletteColors + { + get => (ObservableCollection)this.GetValue(CustomPaletteColorsProperty); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty CustomPaletteColumnCountProperty = + DependencyProperty.Register( + nameof(CustomPaletteColumnCount), + typeof(int), + typeof(ColorPicker), + new PropertyMetadata( + 4, + (s, e) => (s as ColorPicker)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets the number of colors in each row (section) of the custom color palette. + /// Within a standard palette, rows are shades and columns are unique colors. + /// + public int CustomPaletteColumnCount + { + get => (int)this.GetValue(CustomPaletteColumnCountProperty); + set + { + if (object.Equals(value, this.GetValue(CustomPaletteColumnCountProperty)) == false) + { + this.SetValue(CustomPaletteColumnCountProperty, value); + } + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty CustomPaletteProperty = + DependencyProperty.Register( + nameof(CustomPalette), + typeof(IColorPalette), + typeof(ColorPicker), + new PropertyMetadata( + null, + (s, e) => (s as ColorPicker)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets the custom color palette. + /// This will automatically set and + /// overwriting any existing values. + /// + public IColorPalette CustomPalette + { + get => (IColorPalette)this.GetValue(CustomPaletteProperty); + set + { + if (object.Equals(value, this.GetValue(CustomPaletteProperty)) == false) + { + this.SetValue(CustomPaletteProperty, value); + } + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsColorPaletteVisibleProperty = + DependencyProperty.Register( + nameof(IsColorPaletteVisible), + typeof(bool), + typeof(ColorPicker), + new PropertyMetadata( + true, + (s, e) => (s as ColorPicker)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets a value indicating whether the color palette is visible. + /// + public bool IsColorPaletteVisible + { + get => (bool)this.GetValue(IsColorPaletteVisibleProperty); + set + { + if (object.Equals(value, this.GetValue(IsColorPaletteVisibleProperty)) == false) + { + this.SetValue(IsColorPaletteVisibleProperty, value); + } + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ShowAccentColorsProperty = + DependencyProperty.Register( + nameof(ShowAccentColors), + typeof(bool), + typeof(ColorPicker), + new PropertyMetadata( + true, + (s, e) => (s as ColorPicker)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets a value indicating whether accent colors are shown along + /// with the preview color. + /// + public bool ShowAccentColors + { + get => (bool)this.GetValue(ShowAccentColorsProperty); + set + { + if (object.Equals(value, this.GetValue(ShowAccentColorsProperty)) == false) + { + this.SetValue(ShowAccentColorsProperty, value); + } + } + } +} diff --git a/components/ColorPicker/src/ColorPicker.cs b/components/ColorPicker/src/ColorPicker.cs new file mode 100644 index 00000000..cdd0a1a2 --- /dev/null +++ b/components/ColorPicker/src/ColorPicker.cs @@ -0,0 +1,1489 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Controls.Primitives; +using CommunityToolkit.WinUI.Helpers; +using System.Collections.Specialized; +using Windows.UI; +using ColorSpectrum = Microsoft.UI.Xaml.Controls.Primitives.ColorSpectrum; +using ColorPickerSlider = CommunityToolkit.WinUI.Controls.Primitives.ColorPickerSlider; +#if WINAPPSDK || HAS_UNO && WINUI +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Dispatching; +using Colors = Microsoft.UI.Colors; +#elif WINDOWS_UWP || HAS_UNO && WINUI2 +using Windows.System; +using Windows.UI.Xaml.Input; +using Microsoft.UI.Xaml.Controls; +using Colors = Windows.UI.Colors; +#endif +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Presents a color spectrum, a palette of colors, and color channel sliders for user selection of a color. +/// +[TemplatePart(Name = nameof(ColorPicker.AlphaChannelNumberBox), Type = typeof(NumberBox))] +[TemplatePart(Name = nameof(ColorPicker.AlphaChannelSlider), Type = typeof(ColorPickerSlider))] +[TemplatePart(Name = nameof(ColorPicker.Channel1NumberBox), Type = typeof(NumberBox))] +[TemplatePart(Name = nameof(ColorPicker.Channel1Slider), Type = typeof(ColorPickerSlider))] +[TemplatePart(Name = nameof(ColorPicker.Channel2NumberBox), Type = typeof(NumberBox))] +[TemplatePart(Name = nameof(ColorPicker.Channel2Slider), Type = typeof(ColorPickerSlider))] +[TemplatePart(Name = nameof(ColorPicker.Channel3NumberBox), Type = typeof(NumberBox))] +[TemplatePart(Name = nameof(ColorPicker.Channel3Slider), Type = typeof(ColorPickerSlider))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground1Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground2Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground3Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground4Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground5Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground6Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground7Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground8Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground9Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.CheckeredBackground10Border), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPicker.ColorPanelSelector), Type = typeof(ListBox))] +[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))] +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1501:Statement should not be on a single line", Justification = "Inline brackets are used to improve code readability with repeated null checks.")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "Whitespace is used to align code in columns for readability.")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Only template parts start with a capital letter. This differentiates them from other fields.")] +public partial class ColorPicker : Microsoft.UI.Xaml.Controls.ColorPicker +{ + internal Color CheckerBackgroundColor { get; set; } = Color.FromArgb(0x19, 0x80, 0x80, 0x80); // Overridden later + + /// + /// The period that scheduled color updates will be applied. + /// This is only used when updating colors using the ScheduleColorUpdate() method. + /// Color changes made directly to the Color property will apply instantly. + /// + private const int ColorUpdateInterval = 30; // Milliseconds + + private long tokenColor; + + private bool callbacksConnected = false; + private bool eventsConnected = false; + private bool isInitialized = false; + + // Color information for updates + private HsvColor? savedHsvColor = null; + private Color? savedHsvColorRgbEquivalent = null; + private Color? updatedRgbColor = null; + private DispatcherQueueTimer? dispatcherQueueTimer = null; + + private ListBox ColorPanelSelector; + private ColorSpectrum ColorSpectrumControl; + private ColorPickerSlider ColorSpectrumAlphaSlider; + private ColorPickerSlider ColorSpectrumThirdDimensionSlider; + private TextBox HexInputTextBox; + private ToggleButton? HsvToggleButton; + private ToggleButton? RgbToggleButton; + + private NumberBox Channel1NumberBox; + private NumberBox Channel2NumberBox; + private NumberBox Channel3NumberBox; + private NumberBox AlphaChannelNumberBox; + private ColorPickerSlider Channel1Slider; + private ColorPickerSlider Channel2Slider; + private ColorPickerSlider Channel3Slider; + private ColorPickerSlider AlphaChannelSlider; + + private ColorPreviewer ColorPreviewer; + + // Up to 10 checkered backgrounds may be used by name anywhere in the template + private Border CheckeredBackground1Border; + private Border CheckeredBackground2Border; + private Border CheckeredBackground3Border; + private Border CheckeredBackground4Border; + private Border CheckeredBackground5Border; + private Border CheckeredBackground6Border; + private Border CheckeredBackground7Border; + private Border CheckeredBackground8Border; + private Border CheckeredBackground9Border; + private Border CheckeredBackground10Border; + + /*************************************************************************************** + * + * Constructor/Destructor + * + ***************************************************************************************/ + + /// + /// Initializes a new instance of the class. + /// +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public ColorPicker() +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + { + this.DefaultStyleKey = typeof(ColorPicker); + this.DefaultStyleResourceUri = new Uri("ms-appx:///CommunityToolkit.WinUI.Controls.ColorPicker/Themes/Generic.xaml"); + + // Setup collections + this.SetValue(CustomPaletteColorsProperty, new ObservableCollection()); + this.CustomPaletteColors.CollectionChanged += CustomPaletteColors_CollectionChanged; + + this.Loaded += ColorPickerButton_Loaded; + + // Checkered background color is found only one time for performance + // This may need to change in the future if theme changes should be supported + this.CheckerBackgroundColor = (Color)Application.Current.Resources["SystemListLowColor"]; + + this.ConnectCallbacks(true); + this.SetDefaultPalette(); + this.StartDispatcherQueueTimer(); + } + + /// + /// Finalizes an instance of the class. + /// + ~ColorPicker() + { + this.StopDispatcherQueueTimer(); + this.CustomPaletteColors.CollectionChanged -= CustomPaletteColors_CollectionChanged; + } + + /*************************************************************************************** + * + * Methods + * + ***************************************************************************************/ + + /// + /// Gets whether or not the color is considered empty (all fields zero). + /// In the future Color.IsEmpty will hopefully be added to UWP. + /// + /// The Windows.UI.Color to calculate with. + /// Whether the color is considered empty. + private static bool IsColorEmpty(Color color) + { + return color.A == 0x00 && + color.R == 0x00 && + color.G == 0x00 && + color.B == 0x00; + } + + /// + /// Overrides when a template is applied in order to get the required controls. + /// + protected override void OnApplyTemplate() + { + // We need to disconnect old events first + this.ConnectEvents(false); + + this.ColorPanelSelector = this.GetTemplateChild(nameof(ColorPanelSelector)); + + 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.Channel1NumberBox = this.GetTemplateChild(nameof(Channel1NumberBox)); + this.Channel2NumberBox = this.GetTemplateChild(nameof(Channel2NumberBox)); + this.Channel3NumberBox = this.GetTemplateChild(nameof(Channel3NumberBox)); + this.AlphaChannelNumberBox = this.GetTemplateChild(nameof(AlphaChannelNumberBox)); + + 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.ColorPreviewer = this.GetTemplateChild(nameof(ColorPreviewer)); + + 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.ValidateSelectedPanel(); + this.isInitialized = true; + this.SetActiveColorRepresentation(ColorRepresentation.Rgba); + this.UpdateColorControlValues(); // TODO: This will also connect events after, can we optimize vs. doing it twice with the ConnectEvents above? + } + + /// + /// 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 + { +#pragma warning disable CS0413 +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. + T child = this.GetTemplateChild(childName) as T; +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. +#pragma warning restore CS0413 + + if ((child == null) && isRequired) + { + ThrowArgumentNullException(); + } + +#pragma warning disable CS8603 // Possible null reference return. + return child; +#pragma warning restore CS8603 // Possible null reference return. + + static void ThrowArgumentNullException() => throw new ArgumentNullException(nameof(childName)); + } + + /// + /// 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.callbacksConnected = true; + } + else if (connected == false && + this.callbacksConnected == true) + { + // Remove callbacks for dependency properties + this.UnregisterPropertyChangedCallback(ColorProperty, this.tokenColor); + + this.callbacksConnected = false; + } + + return; + } + + /// + /// Connects or disconnects all control event handlers. + /// + /// True to connect event handlers, otherwise false. + private void ConnectEvents(bool connected) + { + if (connected == true && + this.eventsConnected == false) + { + // Add all events + if (this.ColorSpectrumControl != null) { this.ColorSpectrumControl.ColorChanged += ColorSpectrum_ColorChanged; } + if (this.ColorSpectrumControl != null) { this.ColorSpectrumControl.GotFocus += ColorSpectrum_GotFocus; } + if (this.HexInputTextBox != null) { this.HexInputTextBox.KeyDown += HexInputTextBox_KeyDown; } + if (this.HexInputTextBox != null) { this.HexInputTextBox.LostFocus += HexInputTextBox_LostFocus; } + if (this.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.Channel1NumberBox != null) { this.Channel1NumberBox.ValueChanged += ChannelNumberBox_ValueChanged; } + if (this.Channel2NumberBox != null) { this.Channel2NumberBox.ValueChanged += ChannelNumberBox_ValueChanged; } + if (this.Channel3NumberBox != null) { this.Channel3NumberBox.ValueChanged += ChannelNumberBox_ValueChanged; } + if (this.AlphaChannelNumberBox != null) { this.AlphaChannelNumberBox.ValueChanged += ChannelNumberBox_ValueChanged; } + + if (this.Channel1Slider != null) { this.Channel1Slider.ValueChanged += ChannelSlider_ValueChanged; } + if (this.Channel2Slider != null) { this.Channel2Slider.ValueChanged += ChannelSlider_ValueChanged; } + if (this.Channel3Slider != null) { this.Channel3Slider.ValueChanged += ChannelSlider_ValueChanged; } + if (this.AlphaChannelSlider != null) { this.AlphaChannelSlider.ValueChanged += ChannelSlider_ValueChanged; } + if (this.ColorSpectrumAlphaSlider != null) { this.ColorSpectrumAlphaSlider.ValueChanged += ChannelSlider_ValueChanged; } + if (this.ColorSpectrumThirdDimensionSlider != null) { this.ColorSpectrumThirdDimensionSlider.ValueChanged += ChannelSlider_ValueChanged; } + + if (this.Channel1Slider != null) { this.Channel1Slider.Loaded += ChannelSlider_Loaded; } + if (this.Channel2Slider != null) { this.Channel2Slider.Loaded += ChannelSlider_Loaded; } + if (this.Channel3Slider != null) { this.Channel3Slider.Loaded += ChannelSlider_Loaded; } + if (this.AlphaChannelSlider != null) { this.AlphaChannelSlider.Loaded += ChannelSlider_Loaded; } + if (this.ColorSpectrumAlphaSlider != null) { this.ColorSpectrumAlphaSlider.Loaded += ChannelSlider_Loaded; } + if (this.ColorSpectrumThirdDimensionSlider != null) { this.ColorSpectrumThirdDimensionSlider.Loaded += ChannelSlider_Loaded; } + + if (this.ColorPreviewer != null) { this.ColorPreviewer.ColorChangeRequested += ColorPreviewer_ColorChangeRequested; } + + if (this.CheckeredBackground1Border != null) { this.CheckeredBackground1Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground2Border != null) { this.CheckeredBackground2Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground3Border != null) { this.CheckeredBackground3Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground4Border != null) { this.CheckeredBackground4Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground5Border != null) { this.CheckeredBackground5Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground6Border != null) { this.CheckeredBackground6Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground7Border != null) { this.CheckeredBackground7Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground8Border != null) { this.CheckeredBackground8Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground9Border != null) { this.CheckeredBackground9Border.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground10Border != null) { this.CheckeredBackground10Border.Loaded += CheckeredBackgroundBorder_Loaded; } + + this.eventsConnected = true; + } + else if (connected == false && + this.eventsConnected == true) + { + // Remove all events + if (this.ColorSpectrumControl != null) { this.ColorSpectrumControl.ColorChanged -= ColorSpectrum_ColorChanged; } + if (this.ColorSpectrumControl != null) { this.ColorSpectrumControl.GotFocus -= ColorSpectrum_GotFocus; } + if (this.HexInputTextBox != null) { this.HexInputTextBox.KeyDown -= HexInputTextBox_KeyDown; } + if (this.HexInputTextBox != null) { this.HexInputTextBox.LostFocus -= HexInputTextBox_LostFocus; } + if (this.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.Channel1NumberBox != null) { this.Channel1NumberBox.ValueChanged -= ChannelNumberBox_ValueChanged; } + if (this.Channel2NumberBox != null) { this.Channel2NumberBox.ValueChanged -= ChannelNumberBox_ValueChanged; } + if (this.Channel3NumberBox != null) { this.Channel3NumberBox.ValueChanged -= ChannelNumberBox_ValueChanged; } + if (this.AlphaChannelNumberBox != null) { this.AlphaChannelNumberBox.ValueChanged -= ChannelNumberBox_ValueChanged; } + + if (this.Channel1Slider != null) { this.Channel1Slider.ValueChanged -= ChannelSlider_ValueChanged; } + if (this.Channel2Slider != null) { this.Channel2Slider.ValueChanged -= ChannelSlider_ValueChanged; } + if (this.Channel3Slider != null) { this.Channel3Slider.ValueChanged -= ChannelSlider_ValueChanged; } + if (this.AlphaChannelSlider != null) { this.AlphaChannelSlider.ValueChanged -= ChannelSlider_ValueChanged; } + if (this.ColorSpectrumAlphaSlider != null) { this.ColorSpectrumAlphaSlider.ValueChanged -= ChannelSlider_ValueChanged; } + if (this.ColorSpectrumThirdDimensionSlider != null) { this.ColorSpectrumThirdDimensionSlider.ValueChanged -= ChannelSlider_ValueChanged; } + + if (this.Channel1Slider != null) { this.Channel1Slider.Loaded -= ChannelSlider_Loaded; } + if (this.Channel2Slider != null) { this.Channel2Slider.Loaded -= ChannelSlider_Loaded; } + if (this.Channel3Slider != null) { this.Channel3Slider.Loaded -= ChannelSlider_Loaded; } + if (this.AlphaChannelSlider != null) { this.AlphaChannelSlider.Loaded -= ChannelSlider_Loaded; } + if (this.ColorSpectrumAlphaSlider != null) { this.ColorSpectrumAlphaSlider.Loaded -= ChannelSlider_Loaded; } + if (this.ColorSpectrumThirdDimensionSlider != null) { this.ColorSpectrumThirdDimensionSlider.Loaded -= ChannelSlider_Loaded; } + + if (this.ColorPreviewer != null) { this.ColorPreviewer.ColorChangeRequested -= ColorPreviewer_ColorChangeRequested; } + + if (this.CheckeredBackground1Border != null) { this.CheckeredBackground1Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground2Border != null) { this.CheckeredBackground2Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground3Border != null) { this.CheckeredBackground3Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground4Border != null) { this.CheckeredBackground4Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground5Border != null) { this.CheckeredBackground5Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground6Border != null) { this.CheckeredBackground6Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground7Border != null) { this.CheckeredBackground7Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground8Border != null) { this.CheckeredBackground8Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground9Border != null) { this.CheckeredBackground9Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackground10Border != null) { this.CheckeredBackground10Border.Loaded -= CheckeredBackgroundBorder_Loaded; } + + this.eventsConnected = false; + } + + return; + } + + /// + /// Updates all visual states based on current control properties. + /// + /// Whether transitions should occur when changing states. + private void UpdateVisualState(bool useTransitions) + { + 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 Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.SaturationValue: + case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.ValueSaturation: + { + // Hue + return ColorChannel.Channel1; + } + + case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.HueValue: + case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.ValueHue: + { + // Saturation + return ColorChannel.Channel2; + } + + case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.HueSaturation: + case Microsoft.UI.Xaml.Controls.ColorSpectrumComponents.SaturationHue: + { + // Value + return ColorChannel.Channel3; + } + } + + return ColorChannel.Alpha; // Error, should never get here + } + + /// + /// Declares a new color to set to the control. + /// Application of this color will be scheduled to avoid overly rapid updates. + /// + /// The new color to set to the control. + private void ScheduleColorUpdate(Color newColor) + { + // Coerce the value as needed + if (this.IsAlphaEnabled == false) + { + newColor = new Color() + { + R = newColor.R, + G = newColor.G, + B = newColor.B, + A = 255 + }; + } + + this.updatedRgbColor = newColor; + + return; + } + + /// + /// Updates the color values in all editing controls to match the current color. + /// + private void UpdateColorControlValues() + { + bool eventsDisconnectedByMethod = false; + Color rgbColor = this.Color; + HsvColor hsvColor; + + if (this.isInitialized) + { + // Disable events during the update + if (this.eventsConnected) + { + this.ConnectEvents(false); + eventsDisconnectedByMethod = true; + } + + if (this.HexInputTextBox != null) + { + if (this.IsAlphaEnabled) + { + // Remove only the "#" sign + this.HexInputTextBox.Text = rgbColor.ToHex().Replace("#", string.Empty); + } + else + { + // Remove the "#" sign and alpha hex + this.HexInputTextBox.Text = rgbColor.ToHex().Replace("#", string.Empty).Substring(2); + } + } + + // Regardless of the active color representation, the spectrum is always HSV + // Therefore, always calculate HSV color here + // Warning: Always maintain/use HSV information in the saved HSV color + // This avoids loss of precision and drift caused by continuously converting to/from RGB + if (this.savedHsvColor == null) + { + hsvColor = rgbColor.ToHsv(); + + // Round the channels, be sure rounding matches with the scaling next + // Rounding of SVA requires at MINIMUM 2 decimal places + int decimals = 0; + hsvColor = new HsvColor() + { + H = Math.Round(hsvColor.H, decimals), + S = Math.Round(hsvColor.S, 2 + decimals), + V = Math.Round(hsvColor.V, 2 + decimals), + A = Math.Round(hsvColor.A, 2 + decimals) + }; + + // Must update HSV color + this.savedHsvColor = hsvColor; + this.savedHsvColorRgbEquivalent = rgbColor; + } + else + { + hsvColor = this.savedHsvColor.Value; + } + + // Update the color spectrum + // Remember the spectrum is always HSV and must be updated as such to avoid + // conversion errors + if (this.ColorSpectrumControl != null) + { + this.ColorSpectrumControl.HsvColor = new System.Numerics.Vector4() + { + X = Convert.ToSingle(hsvColor.H), + Y = Convert.ToSingle(hsvColor.S), + Z = Convert.ToSingle(hsvColor.V), + W = Convert.ToSingle(hsvColor.A) + }; + } + + // Update the color spectrum third dimension channel + if (this.ColorSpectrumThirdDimensionSlider != null) + { + // Convert the channels into a usable range for the user + double hue = hsvColor.H; + double staturation = hsvColor.S * 100; + double value = hsvColor.V * 100; + + switch (this.GetActiveColorSpectrumThirdDimension()) + { + case ColorChannel.Channel1: + { + // Hue + this.ColorSpectrumThirdDimensionSlider.Minimum = 0; + this.ColorSpectrumThirdDimensionSlider.Maximum = 360; + this.ColorSpectrumThirdDimensionSlider.Value = hue; + break; + } + + case ColorChannel.Channel2: + { + // Saturation + this.ColorSpectrumThirdDimensionSlider.Minimum = 0; + this.ColorSpectrumThirdDimensionSlider.Maximum = 100; + this.ColorSpectrumThirdDimensionSlider.Value = staturation; + break; + } + + case ColorChannel.Channel3: + { + // Value + this.ColorSpectrumThirdDimensionSlider.Minimum = 0; + this.ColorSpectrumThirdDimensionSlider.Maximum = 100; + this.ColorSpectrumThirdDimensionSlider.Value = value; + break; + } + } + } + + // Update the preview color + if (this.ColorPreviewer != null) + { + this.ColorPreviewer.HsvColor = hsvColor; + } + + // Update all other color channels + if (this.GetActiveColorRepresentation() == ColorRepresentation.Hsva) + { + // Convert the channels into a usable range for the user + double hue = hsvColor.H; + double staturation = hsvColor.S * 100; + double value = hsvColor.V * 100; + double alpha = hsvColor.A * 100; + + // Hue + if (this.Channel1NumberBox != null) + { + this.Channel1NumberBox.Minimum = 0; + this.Channel1NumberBox.Maximum = 360; + this.Channel1NumberBox.Value = hue; + } + + if (this.Channel1Slider != null) + { + this.Channel1Slider.Minimum = 0; + this.Channel1Slider.Maximum = 360; + this.Channel1Slider.Value = hue; + } + + // Saturation + if (this.Channel2NumberBox != null) + { + this.Channel2NumberBox.Minimum = 0; + this.Channel2NumberBox.Maximum = 100; + this.Channel2NumberBox.Value = staturation; + } + + if (this.Channel2Slider != null) + { + this.Channel2Slider.Minimum = 0; + this.Channel2Slider.Maximum = 100; + this.Channel2Slider.Value = staturation; + } + + // Value + if (this.Channel3NumberBox != null) + { + this.Channel3NumberBox.Minimum = 0; + this.Channel3NumberBox.Maximum = 100; + this.Channel3NumberBox.Value = value; + } + + if (this.Channel3Slider != null) + { + this.Channel3Slider.Minimum = 0; + this.Channel3Slider.Maximum = 100; + this.Channel3Slider.Value = value; + } + + // Alpha + if (this.AlphaChannelNumberBox != null) + { + this.AlphaChannelNumberBox.Minimum = 0; + this.AlphaChannelNumberBox.Maximum = 100; + this.AlphaChannelNumberBox.Value = alpha; + } + + if (this.AlphaChannelSlider != null) + { + this.AlphaChannelSlider.Minimum = 0; + this.AlphaChannelSlider.Maximum = 100; + this.AlphaChannelSlider.Value = alpha; + } + + // Color spectrum alpha + if (this.ColorSpectrumAlphaSlider != null) + { + this.ColorSpectrumAlphaSlider.Minimum = 0; + this.ColorSpectrumAlphaSlider.Maximum = 100; + this.ColorSpectrumAlphaSlider.Value = alpha; + } + } + else + { + // Red + if (this.Channel1NumberBox != null) + { + this.Channel1NumberBox.Minimum = 0; + this.Channel1NumberBox.Maximum = 255; + this.Channel1NumberBox.Value = Convert.ToDouble(rgbColor.R); + } + + if (this.Channel1Slider != null) + { + this.Channel1Slider.Minimum = 0; + this.Channel1Slider.Maximum = 255; + this.Channel1Slider.Value = Convert.ToDouble(rgbColor.R); + } + + // Green + if (this.Channel2NumberBox != null) + { + this.Channel2NumberBox.Minimum = 0; + this.Channel2NumberBox.Maximum = 255; + this.Channel2NumberBox.Value = Convert.ToDouble(rgbColor.G); + } + + if (this.Channel2Slider != null) + { + this.Channel2Slider.Minimum = 0; + this.Channel2Slider.Maximum = 255; + this.Channel2Slider.Value = Convert.ToDouble(rgbColor.G); + } + + // Blue + if (this.Channel3NumberBox != null) + { + this.Channel3NumberBox.Minimum = 0; + this.Channel3NumberBox.Maximum = 255; + this.Channel3NumberBox.Value = Convert.ToDouble(rgbColor.B); + } + + if (this.Channel3Slider != null) + { + this.Channel3Slider.Minimum = 0; + this.Channel3Slider.Maximum = 255; + this.Channel3Slider.Value = Convert.ToDouble(rgbColor.B); + } + + // Alpha + if (this.AlphaChannelNumberBox != null) + { + this.AlphaChannelNumberBox.Minimum = 0; + this.AlphaChannelNumberBox.Maximum = 255; + this.AlphaChannelNumberBox.Value = Convert.ToDouble(rgbColor.A); + } + + if (this.AlphaChannelSlider != null) + { + this.AlphaChannelSlider.Minimum = 0; + this.AlphaChannelSlider.Maximum = 255; + this.AlphaChannelSlider.Value = Convert.ToDouble(rgbColor.A); + } + + // Color spectrum alpha + if (this.ColorSpectrumAlphaSlider != null) + { + this.ColorSpectrumAlphaSlider.Minimum = 0; + this.ColorSpectrumAlphaSlider.Maximum = 255; + this.ColorSpectrumAlphaSlider.Value = Convert.ToDouble(rgbColor.A); + } + } + + if (eventsDisconnectedByMethod) + { + this.ConnectEvents(true); + } + } + + return; + } + + /// + /// Sets a new color channel value to the current color. + /// Only the specified color channel will be modified. + /// + /// The color representation of the given channel. + /// The specified color channel to modify. + /// The new color channel value. + private void SetColorChannel( + ColorRepresentation colorRepresentation, + ColorChannel channel, + double newValue) + { + Color oldRgbColor = this.Color; + Color newRgbColor; + HsvColor oldHsvColor; + + if (colorRepresentation == ColorRepresentation.Hsva) + { + // Warning: Always maintain/use HSV information in the saved HSV color + // This avoids loss of precision and drift caused by continuously converting to/from RGB + if (this.savedHsvColor == null) + { + oldHsvColor = oldRgbColor.ToHsv(); + } + else + { + oldHsvColor = this.savedHsvColor.Value; + } + + double hue = oldHsvColor.H; + double saturation = oldHsvColor.S; + double value = oldHsvColor.V; + double alpha = oldHsvColor.A; + + switch (channel) + { + case ColorChannel.Channel1: + { + hue = Math.Clamp(double.IsNaN(newValue) ? 0 : newValue, 0, 360); + break; + } + + case ColorChannel.Channel2: + { + saturation = Math.Clamp((double.IsNaN(newValue) ? 0 : newValue) / 100, 0, 1); + break; + } + + case ColorChannel.Channel3: + { + value = Math.Clamp((double.IsNaN(newValue) ? 0 : newValue) / 100, 0, 1); + break; + } + + case ColorChannel.Alpha: + { + // Unlike color channels, default to no transparency + alpha = Math.Clamp((double.IsNaN(newValue) ? 100 : newValue) / 100, 0, 1); + break; + } + } + + newRgbColor = Helpers.ColorHelper.FromHsv( + hue, + saturation, + value, + alpha); + + // Must update HSV color + this.savedHsvColor = new HsvColor() + { + H = hue, + S = saturation, + V = value, + A = alpha + }; + this.savedHsvColorRgbEquivalent = newRgbColor; + } + else + { + byte red = oldRgbColor.R; + byte green = oldRgbColor.G; + byte blue = oldRgbColor.B; + byte alpha = oldRgbColor.A; + + switch (channel) + { + case ColorChannel.Channel1: + { + red = Convert.ToByte(Math.Clamp(double.IsNaN(newValue) ? 0 : newValue, 0, 255)); + break; + } + + case ColorChannel.Channel2: + { + green = Convert.ToByte(Math.Clamp(double.IsNaN(newValue) ? 0 : newValue, 0, 255)); + break; + } + + case ColorChannel.Channel3: + { + blue = Convert.ToByte(Math.Clamp(double.IsNaN(newValue) ? 0 : newValue, 0, 255)); + break; + } + + case ColorChannel.Alpha: + { + // Unlike color channels, default to no transparency + alpha = Convert.ToByte(Math.Clamp(double.IsNaN(newValue) ? 255 : newValue, 0, 255)); + break; + } + } + + newRgbColor = new Color() + { + R = red, + G = green, + B = blue, + A = alpha + }; + + // Must clear saved HSV color + this.savedHsvColor = null; + this.savedHsvColorRgbEquivalent = null; + } + + this.ScheduleColorUpdate(newRgbColor); + return; + } + + /// + /// Updates all channel slider control backgrounds. + /// + private void UpdateChannelSliderBackgrounds() + { + this.UpdateChannelSliderBackground(this.Channel1Slider); + this.UpdateChannelSliderBackground(this.Channel2Slider); + this.UpdateChannelSliderBackground(this.Channel3Slider); + this.UpdateChannelSliderBackground(this.AlphaChannelSlider); + this.UpdateChannelSliderBackground(this.ColorSpectrumAlphaSlider); + this.UpdateChannelSliderBackground(this.ColorSpectrumThirdDimensionSlider); + return; + } + + /// + /// Updates a specific channel slider control background. + /// + /// The color channel slider to update the background for. + private void UpdateChannelSliderBackground(ColorPickerSlider slider) + { + if (slider != null) + { + // Regardless of the active color representation, the sliders always use HSV + // Therefore, always calculate HSV color here + // Warning: Always maintain/use HSV information in the saved HSV color + // This avoids loss of precision and drift caused by continuously converting to/from RGB + if (this.savedHsvColor == null) + { + var rgbColor = this.Color; + + // Must update HSV color + this.savedHsvColor = rgbColor.ToHsv(); + this.savedHsvColorRgbEquivalent = rgbColor; + } + + slider.IsAutoUpdatingEnabled = false; + + if (object.ReferenceEquals(slider, this.Channel1Slider)) + { + slider.ColorChannel = ColorChannel.Channel1; + slider.ColorRepresentation = this.GetActiveColorRepresentation(); + } + else if (object.ReferenceEquals(slider, this.Channel2Slider)) + { + slider.ColorChannel = ColorChannel.Channel2; + slider.ColorRepresentation = this.GetActiveColorRepresentation(); + } + else if (object.ReferenceEquals(slider, this.Channel3Slider)) + { + slider.ColorChannel = ColorChannel.Channel3; + slider.ColorRepresentation = this.GetActiveColorRepresentation(); + } + else if (object.ReferenceEquals(slider, this.AlphaChannelSlider)) + { + slider.ColorChannel = ColorChannel.Alpha; + slider.ColorRepresentation = this.GetActiveColorRepresentation(); + } + else if (object.ReferenceEquals(slider, this.ColorSpectrumAlphaSlider)) + { + slider.ColorChannel = ColorChannel.Alpha; + slider.ColorRepresentation = this.GetActiveColorRepresentation(); + } + else if (object.ReferenceEquals(slider, this.ColorSpectrumThirdDimensionSlider)) + { + slider.ColorChannel = this.GetActiveColorSpectrumThirdDimension(); + slider.ColorRepresentation = ColorRepresentation.Hsva; // Always HSV + } + + slider.HsvColor = this.savedHsvColor.Value; + slider.UpdateColors(); + } + + return; + } + + /// + /// Sets the default color palette to the control. + /// + private void SetDefaultPalette() + { + this.CustomPalette = new FluentColorPalette(); + + return; + } + + /// + /// Validates and updates the current 'tab' or 'panel' selection. + /// If the currently selected tab is collapsed, the next visible tab will be selected. + /// + private void ValidateSelectedPanel() + { + object? selectedItem = null; + + if (this.ColorPanelSelector != null) + { + if (this.ColorPanelSelector.SelectedItem == null && + this.ColorPanelSelector.Items.Count > 0) + { + // As a failsafe, forcefully select the first item + selectedItem = this.ColorPanelSelector.Items[0]; + } + else + { + selectedItem = this.ColorPanelSelector.SelectedItem; + } + + if (selectedItem is UIElement selectedElement && + selectedElement.Visibility == Visibility.Collapsed) + { + // Select the first visible item instead + foreach (object item in this.ColorPanelSelector.Items) + { + if (item is UIElement element && + element.Visibility == Visibility.Visible) + { + selectedItem = item; + break; + } + } + } + + this.ColorPanelSelector.SelectedItem = selectedItem; + } + + return; + } + + private void OnDependencyPropertyChanged(object sender, DependencyPropertyChangedEventArgs args) + { + if (sender is DependencyObject senderControl) + { + /* Note: ColorProperty is defined in the base class and cannot be used here + * See the OnColorChanged callback below + */ + + if (object.ReferenceEquals(args.Property, CustomPaletteProperty)) + { + IColorPalette palette = this.CustomPalette; + + if (palette != null) + { + this.CustomPaletteColumnCount = palette.ColorCount; + this.CustomPaletteColors.Clear(); + + for (int shadeIndex = 0; shadeIndex < palette.ShadeCount; shadeIndex++) + { + for (int colorIndex = 0; colorIndex < palette.ColorCount; colorIndex++) + { + this.CustomPaletteColors.Add(palette.GetColor(colorIndex, shadeIndex)); + } + } + } + } + else if (object.ReferenceEquals(args.Property, IsColorPaletteVisibleProperty)) + { + this.UpdateVisualState(false); + this.ValidateSelectedPanel(); + } + } + + return; + } + + /*************************************************************************************** + * + * Color Update Timer + * + ***************************************************************************************/ + + private void StartDispatcherQueueTimer() + { + this.dispatcherQueueTimer = DispatcherQueue.GetForCurrentThread().CreateTimer(); + this.dispatcherQueueTimer.Interval = new TimeSpan(0, 0, 0, 0, ColorUpdateInterval); + this.dispatcherQueueTimer.Tick += DispatcherQueueTimer_Tick; + this.dispatcherQueueTimer.Start(); + + return; + } + + private void StopDispatcherQueueTimer() + { + if (this.dispatcherQueueTimer != null) + { + this.dispatcherQueueTimer.Stop(); + } + + return; + } + + private void DispatcherQueueTimer_Tick(object sender, object e) + { + if (this.updatedRgbColor != null) + { + var newColor = this.updatedRgbColor.Value; + + // Clear first to avoid timing issues if it takes longer than the timer interval to set the new color + this.updatedRgbColor = null; + + // An equality check here is important + // Without it, OnColorChanged would continuously be invoked and preserveHsvColor overwritten when not wanted + if (object.Equals(newColor, this.GetValue(ColorProperty)) == false) + { + // Disable events here so the color update isn't repeated as other controls in the UI are updated through binding. + // For example, the Spectrum should be bound to Color, as soon as Color is changed here the Spectrum is updated. + // Then however, the ColorSpectrum.ColorChanged event would fire which would schedule a new color update -- + // with the same color. This causes several problems: + // 1. Layout cycle that may crash the app + // 2. A performance hit recalculating for no reason + // 3. preserveHsvColor gets overwritten unexpectedly by the ColorChanged handler + this.ConnectEvents(false); + this.SetValue(ColorProperty, newColor); + this.ConnectEvents(true); + } + } + + return; + } + + /*************************************************************************************** + * + * Callbacks + * + ***************************************************************************************/ + + /// + /// Callback for when the dependency property value changes. + /// + private void OnColorChanged(DependencyObject d, DependencyProperty e) + { + // TODO: Coerce the value if Alpha is disabled, is this handled in the base ColorPicker? + if ((this.savedHsvColor != null) && + (object.Equals(d.GetValue(e), this.savedHsvColorRgbEquivalent) == false)) + { + // The color was updated from an unknown source + // The RGB and HSV colors are no longer in sync so the HSV color must be cleared + this.savedHsvColor = null; + this.savedHsvColorRgbEquivalent = null; + } + + this.UpdateColorControlValues(); + this.UpdateChannelSliderBackgrounds(); + + return; + } + + /*************************************************************************************** + * + * Event Handling + * + ***************************************************************************************/ + + /// + /// Event handler for when the control has finished loaded. + /// + private void ColorPickerButton_Loaded(object sender, RoutedEventArgs e) + { + // Available but not currently used + return; + } + + /// + /// Event handler for when a color channel slider is loaded. + /// This will draw an initial background. + /// + private void ChannelSlider_Loaded(object sender, RoutedEventArgs e) + { + if (sender is ColorPickerSlider slider) + { + this.UpdateChannelSliderBackground(slider); + } + return; + } + + /// + /// Event handler to draw checkered backgrounds on-demand as controls are loaded. + /// + private async void CheckeredBackgroundBorder_Loaded(object sender, RoutedEventArgs e) + { + if (sender is Border border) + { + await ColorPickerRenderingHelpers.UpdateBorderBackgroundWithCheckerAsync( + border, + CheckerBackgroundColor); + } + + } + + /// + /// Event handler for when the list of custom palette colors is changed. + /// + private void CustomPaletteColors_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + // Available but not currently used + return; + } + + /// + /// Event handler for when the color spectrum color is changed. + /// This occurs when the user presses on the spectrum to select a new color. + /// + private void ColorSpectrum_ColorChanged(ColorSpectrum sender, Microsoft.UI.Xaml.Controls.ColorChangedEventArgs args) + { + // It is OK in this case to use the RGB representation + this.ScheduleColorUpdate(this.ColorSpectrumControl.Color); + return; + } + + /// + /// Event handler for when the color spectrum is focused. + /// This is used only to work around some bugs that cause usability problems. + /// + private void ColorSpectrum_GotFocus(object sender, RoutedEventArgs e) + { + Color rgbColor = this.ColorSpectrumControl.Color; + + /* If this control has a color that is currently empty (#00000000), + * selecting a new color directly in the spectrum will fail. This is + * a bug in the color spectrum. Selecting a new color in the spectrum will + * keep zero for all channels (including alpha and the third dimension). + * + * In practice this means a new color cannot be selected using the spectrum + * until both the alpha and third dimension slider are raised above zero. + * This is extremely user unfriendly and must be corrected as best as possible. + * + * In order to work around this, detect when the color spectrum has selected + * a new color and then automatically set the alpha and third dimension + * channel to maximum. However, the color spectrum has a second bug, the + * ColorChanged event is never raised if the color is empty. This prevents + * automatically setting the other channels where it normally should be done + * (in the ColorChanged event). + * + * In order to work around this second bug, the GotFocus event is used + * to detect when the spectrum is engaged by the user. It's somewhat equivalent + * to ColorChanged for this purpose. Then when the GotFocus event is fired + * set the alpha and third channel values to maximum. The problem here is that + * the GotFocus event does not have access to the new color that was selected + * in the spectrum. It is not available due to the afore mentioned bug or due to + * timing. This means the best that can be done is to just set a 'neutral' + * color such as white. + * + * There is still a small usability issue with this as it requires two + * presses to set a color. That's far better than having to slide up both + * sliders though. + * + * 1. If the color is empty, the first press on the spectrum will set white + * and ignore the pressed color on the spectrum + * 2. The second press on the spectrum will be correctly handled. + * + */ + + // In the future Color.IsEmpty will hopefully be added to UWP + if (IsColorEmpty(rgbColor)) + { + /* The following code may be used in the future if ever the selected color is available + + Color newColor = this.ColorSpectrum.Color; + HsvColor newHsvColor = newColor.ToHsv(); + + switch (this.GetActiveColorSpectrumThirdDimension()) + { + case ColorChannel.Channel1: + { + newColor = Microsoft.Toolkit.Uwp.Helpers.ColorHelper.FromHsv + ( + 360.0, + newHsvColor.S, + newHsvColor.V, + 100.0 + ); + break; + } + + case ColorChannel.Channel2: + { + newColor = Microsoft.Toolkit.Uwp.Helpers.ColorHelper.FromHsv + ( + newHsvColor.H, + 100.0, + newHsvColor.V, + 100.0 + ); + break; + } + + case ColorChannel.Channel3: + { + newColor = Microsoft.Toolkit.Uwp.Helpers.ColorHelper.FromHsv + ( + newHsvColor.H, + newHsvColor.S, + 100.0, + 100.0 + ); + break; + } + } + */ + + this.ScheduleColorUpdate(Colors.White); + } + else if (rgbColor.A == 0x00) + { + // As an additional usability improvement, reset alpha to maximum when the spectrum is used. + // The color spectrum has no alpha channel and it is much more intuitive to do this for the user + // especially if the picker was initially set with Colors.Transparent. + this.ScheduleColorUpdate(Color.FromArgb(0xFF, rgbColor.R, rgbColor.G, rgbColor.B)); + } + + return; + } + + /// + /// Event handler for when the selected color representation changes. + /// This will convert between RGB and HSV. + /// + private void 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 the color previewer requests a new color. + /// + private void ColorPreviewer_ColorChangeRequested(object? sender, HsvColor hsvColor) + { + Color rgbColor = Helpers.ColorHelper.FromHsv(hsvColor.H, hsvColor.S, hsvColor.V, hsvColor.A); + + // Regardless of the active color model, the previewer always uses HSV + // Therefore, always calculate HSV color here + // Warning: Always maintain/use HSV information in the saved HSV color + // This avoids loss of precision and drift caused by continuously converting to/from RGB + this.savedHsvColor = hsvColor; + this.savedHsvColorRgbEquivalent = rgbColor; + + this.ScheduleColorUpdate(rgbColor); + + return; + } + + /// + /// Event handler for when a key is pressed within the Hex RGB value TextBox. + /// This is used to trigger a re-evaluation of the color based on the TextBox value. + /// + private void HexInputTextBox_KeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key == Windows.System.VirtualKey.Enter) + { + try + { + ColorToHexConverter converter = new ColorToHexConverter(); + this.Color = (Color)converter.ConvertBack(((TextBox)sender).Text, typeof(TextBox), null, null); + } + catch + { + // Reset hex value + this.UpdateColorControlValues(); + this.UpdateChannelSliderBackgrounds(); + } + } + + return; + } + + /// + /// Event handler for when the Hex RGB value TextBox looses focus. + /// This is used to trigger a re-evaluation of the color based on the TextBox value. + /// + private void HexInputTextBox_LostFocus(object sender, RoutedEventArgs e) + { + try + { + ColorToHexConverter converter = new ColorToHexConverter(); + this.Color = (Color)converter.ConvertBack(((TextBox)sender).Text, typeof(TextBox), null, null); + } + catch + { + // Reset hex value + this.UpdateColorControlValues(); + this.UpdateChannelSliderBackgrounds(); + } + + return; + } + + /// + /// Event handler for when the value within one of the channel NumberBoxes is changed. + /// + private void ChannelNumberBox_ValueChanged(NumberBox sender, NumberBoxValueChangedEventArgs args) + { + double senderValue = sender.Value; + + if (object.ReferenceEquals(sender, this.Channel1NumberBox)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel1, senderValue); + } + else if (object.ReferenceEquals(sender, this.Channel2NumberBox)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel2, senderValue); + } + else if (object.ReferenceEquals(sender, this.Channel3NumberBox)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel3, senderValue); + } + else if (object.ReferenceEquals(sender, this.AlphaChannelNumberBox)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Alpha, senderValue); + } + + return; + } + + /// + /// Event handler for when the value within one of the channel Sliders is changed. + /// + private void ChannelSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e) + { + double senderValue = (sender as Slider)?.Value ?? double.NaN; + + if (object.ReferenceEquals(sender, this.Channel1Slider)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel1, senderValue); + } + else if (object.ReferenceEquals(sender, this.Channel2Slider)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel2, senderValue); + } + else if (object.ReferenceEquals(sender, this.Channel3Slider)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Channel3, senderValue); + } + else if (object.ReferenceEquals(sender, this.AlphaChannelSlider)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Alpha, senderValue); + } + else if (object.ReferenceEquals(sender, this.ColorSpectrumAlphaSlider)) + { + this.SetColorChannel(this.GetActiveColorRepresentation(), ColorChannel.Alpha, senderValue); + } + else if (object.ReferenceEquals(sender, this.ColorSpectrumThirdDimensionSlider)) + { + // Regardless of the active color representation, the spectrum is always HSV + this.SetColorChannel(ColorRepresentation.Hsva, this.GetActiveColorSpectrumThirdDimension(), senderValue); + } + + return; + } +} diff --git a/components/ColorPicker/src/ColorPicker.xaml b/components/ColorPicker/src/ColorPicker.xaml new file mode 100644 index 00000000..eac639eb --- /dev/null +++ b/components/ColorPicker/src/ColorPicker.xaml @@ -0,0 +1,1251 @@ + + + + + Transparent + + + + + #70F5F5F5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/ColorPicker/src/ColorPickerButton.cs b/components/ColorPicker/src/ColorPickerButton.cs new file mode 100644 index 00000000..5b8d80d4 --- /dev/null +++ b/components/ColorPicker/src/ColorPickerButton.cs @@ -0,0 +1,161 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Windows.UI; + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// A which displays a color as its Content and it's Flyout is a . +/// +[TemplatePart(Name = nameof(CheckeredBackgroundBorder), Type = typeof(Border))] +public partial class ColorPickerButton : DropDownButton +{ + /// + /// Gets the instances contained by the . + /// + public ColorPicker ColorPicker { get; private set; } + + /// + /// Gets or sets the for the control used in the button. + /// + public Style ColorPickerStyle + { + get + { + return (Style)GetValue(ColorPickerStyleProperty); + } + + set + { + SetValue(ColorPickerStyleProperty, value); + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ColorPickerStyleProperty = DependencyProperty.Register("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, new PropertyChangedCallback(SelectedColorChanged))); + +#pragma warning disable SA1306 // Field names should begin with lower-case letter + //// Template Parts + private Border? CheckeredBackgroundBorder; +#pragma warning restore SA1306 // Field names should begin with lower-case letter + + /// + /// Initializes a new instance of the class. + /// +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public ColorPickerButton() +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + { + this.DefaultStyleKey = typeof(ColorPickerButton); + } + + /// + protected override void OnApplyTemplate() + { + if (ColorPicker != null) + { + ColorPicker.ColorChanged -= ColorPicker_ColorChanged; + } + + base.OnApplyTemplate(); + + if (ColorPickerStyle != null) + { + ColorPicker = new ColorPicker() { Style = ColorPickerStyle }; + } + else + { + ColorPicker = new ColorPicker(); + } + + ColorPicker.Color = SelectedColor; + ColorPicker.ColorChanged += ColorPicker_ColorChanged; + + if (Flyout == null) + { + Flyout = new Flyout() + { + // TODO: Expose Placement + Placement = FlyoutPlacementMode.BottomEdgeAlignedLeft, + FlyoutPresenterStyle = FlyoutPresenterStyle, + Content = ColorPicker + }; + } + + if (CheckeredBackgroundBorder != null) + { + CheckeredBackgroundBorder.Loaded -= this.CheckeredBackgroundBorder_Loaded; + } + + CheckeredBackgroundBorder = GetTemplateChild(nameof(CheckeredBackgroundBorder)) as Border; + + if (CheckeredBackgroundBorder != null) + { + CheckeredBackgroundBorder.Loaded += this.CheckeredBackgroundBorder_Loaded; + } + } + + private static void SelectedColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ColorPickerButton instance && !(instance.ColorPicker is null)) + { + instance.ColorPicker.Color = instance.SelectedColor; + } + } + + private void ColorPicker_ColorChanged(Microsoft.UI.Xaml.Controls.ColorPicker sender, Microsoft.UI.Xaml.Controls.ColorChangedEventArgs args) + { + SelectedColor = args.NewColor; + } + + private async void CheckeredBackgroundBorder_Loaded(object sender, RoutedEventArgs e) + { + if (sender is Border border) + { + await ColorPickerRenderingHelpers.UpdateBorderBackgroundWithCheckerAsync(border, + ColorPicker.CheckerBackgroundColor); // TODO: Check initialization + } + } +} diff --git a/components/ColorPicker/src/ColorPickerButton.xaml b/components/ColorPicker/src/ColorPickerButton.xaml new file mode 100644 index 00000000..9a323876 --- /dev/null +++ b/components/ColorPicker/src/ColorPickerButton.xaml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/ColorPicker/src/ColorPickerRenderingHelpers.cs b/components/ColorPicker/src/ColorPickerRenderingHelpers.cs new file mode 100644 index 00000000..7ac31a24 --- /dev/null +++ b/components/ColorPicker/src/ColorPickerRenderingHelpers.cs @@ -0,0 +1,536 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Windows.UI; +using Microsoft.UI; +#if WINAPPSDK +using Microsoft.UI.Xaml.Media.Imaging; +using Colors = Microsoft.UI.Colors; +#else +using Windows.UI.Xaml.Media.Imaging; +using Colors = Windows.UI.Colors; +#endif + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Contains the rendering methods used within . +/// +internal class ColorPickerRenderingHelpers +{ + /// + /// Gets the default color used for checkered background squares (alternate squares are transparent). + /// Checkered backgrounds are used to help show transparency. + /// + internal static readonly Color CheckerBackgroundColor = Color.FromArgb(0x19, 0x80, 0x80, 0x80); + + /// + /// Generates a new bitmap of the specified size by changing a specific color channel. + /// This will produce a gradient representing all possible differences of that color channel. + /// + /// The pixel width (X, horizontal) of the resulting bitmap. + /// The pixel height (Y, vertical) of the resulting bitmap. + /// The orientation of the resulting bitmap (gradient direction). + /// The color representation being used: RGBA or HSVA. + /// The specific color channel to vary. + /// The base HSV color used for channels not being changed. + /// The color of the checker background square. + /// Fix the alpha channel value to maximum during calculation. + /// This will remove any alpha/transparency from the other channel backgrounds. + /// Fix the saturation and value channels to maximum + /// during calculation in HSVA color representation. + /// This will ensure colors are always discernible regardless of saturation/value. + /// A new bitmap representing a gradient of color channel values. + public static async Task CreateChannelBitmapAsync( + int width, + int height, + Orientation orientation, + ColorRepresentation colorRepresentation, + ColorChannel channel, + HsvColor baseHsvColor, + Color? checkerColor, + bool isAlphaMaxForced, + bool isSaturationValueMaxForced) + { + if (width == 0 || height == 0) + { + return null!; + } + + var bitmap = await Task.Run(async () => + { + int pixelDataIndex = 0; + double channelStep; + byte[] bgraPixelData; + byte[]? bgraCheckeredPixelData = null; + Color baseRgbColor = Colors.White; + Color rgbColor; + int bgraPixelDataHeight; + int bgraPixelDataWidth; + + // Allocate the buffer + // BGRA formatted color channels 1 byte each (4 bytes in a pixel) + bgraPixelData = new byte[width * height * 4]; + bgraPixelDataHeight = height * 4; + bgraPixelDataWidth = width * 4; + + // Maximize alpha channel value + if (isAlphaMaxForced && + channel != ColorChannel.Alpha) + { + baseHsvColor = new HsvColor() + { + H = baseHsvColor.H, + S = baseHsvColor.S, + V = baseHsvColor.V, + A = 1.0 + }; + } + + // Convert HSV to RGB once + if (colorRepresentation == ColorRepresentation.Rgba) + { + baseRgbColor = Helpers.ColorHelper.FromHsv( + baseHsvColor.H, + baseHsvColor.S, + baseHsvColor.V, + baseHsvColor.A); + } + + // Maximize Saturation and Value channels when in HSVA mode + if (isSaturationValueMaxForced && + colorRepresentation == ColorRepresentation.Hsva && + channel != ColorChannel.Alpha) + { + switch (channel) + { + case ColorChannel.Channel1: + baseHsvColor = new HsvColor() + { + H = baseHsvColor.H, + S = 1.0, + V = 1.0, + A = baseHsvColor.A + }; + break; + case ColorChannel.Channel2: + baseHsvColor = new HsvColor() + { + H = baseHsvColor.H, + S = baseHsvColor.S, + V = 1.0, + A = baseHsvColor.A + }; + break; + case ColorChannel.Channel3: + baseHsvColor = new HsvColor() + { + H = baseHsvColor.H, + S = 1.0, + V = baseHsvColor.V, + A = baseHsvColor.A + }; + break; + } + } + + // Create a checkered background + if (checkerColor != null) + { + bgraCheckeredPixelData = await CreateCheckeredBitmapAsync( + width, + height, + checkerColor.Value); + } + + // Create the color channel gradient + if (orientation == Orientation.Horizontal) + { + // Determine the numerical increment of the color steps within the channel + if (colorRepresentation == ColorRepresentation.Hsva) + { + if (channel == ColorChannel.Channel1) + { + channelStep = 360.0 / width; + } + else + { + channelStep = 1.0 / width; + } + } + else + { + channelStep = 255.0 / width; + } + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + if (y == 0) + { + rgbColor = GetColor(x * channelStep); + + // Get a new color + bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(rgbColor.B * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(rgbColor.G * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(rgbColor.R * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 3] = rgbColor.A; + } + else + { + // Use the color in the row above + // Remember the pixel data is 1 dimensional instead of 2 + bgraPixelData[pixelDataIndex + 0] = bgraPixelData[pixelDataIndex + 0 - bgraPixelDataWidth]; + bgraPixelData[pixelDataIndex + 1] = bgraPixelData[pixelDataIndex + 1 - bgraPixelDataWidth]; + bgraPixelData[pixelDataIndex + 2] = bgraPixelData[pixelDataIndex + 2 - bgraPixelDataWidth]; + bgraPixelData[pixelDataIndex + 3] = bgraPixelData[pixelDataIndex + 3 - bgraPixelDataWidth]; + } + + pixelDataIndex += 4; + } + } + } + else + { + // Determine the numerical increment of the color steps within the channel + if (colorRepresentation == ColorRepresentation.Hsva) + { + if (channel == ColorChannel.Channel1) + { + channelStep = 360.0 / height; + } + else + { + channelStep = 1.0 / height; + } + } + else + { + channelStep = 255.0 / height; + } + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + if (x == 0) + { + // The lowest channel value should be at the 'bottom' of the bitmap + rgbColor = GetColor((height - 1 - y) * channelStep); + + // Get a new color + bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(rgbColor.B * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(rgbColor.G * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(rgbColor.R * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 3] = rgbColor.A; + } + else + { + // Use the color in the column to the left + // Remember the pixel data is 1 dimensional instead of 2 + bgraPixelData[pixelDataIndex + 0] = bgraPixelData[pixelDataIndex - 4]; + bgraPixelData[pixelDataIndex + 1] = bgraPixelData[pixelDataIndex - 3]; + bgraPixelData[pixelDataIndex + 2] = bgraPixelData[pixelDataIndex - 2]; + bgraPixelData[pixelDataIndex + 3] = bgraPixelData[pixelDataIndex - 1]; + } + + pixelDataIndex += 4; + } + } + } + + // Composite the checkered background with color channel gradient for final result + // The height/width are not checked as both bitmaps were built with the same values + if ((checkerColor != null) && + (bgraCheckeredPixelData != null)) + { + pixelDataIndex = 0; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + /* The following algorithm is used to blend the two bitmaps creating the final composite. + * In this formula, pixel data is normalized 0..1, actual pixel data is in the range 0..255. + * The color channel gradient should apply OVER the checkered background. + * + * R = R0 * A0 * (1 - A1) + R1 * A1 = RA0 * (1 - A1) + RA1 + * G = G0 * A0 * (1 - A1) + G1 * A1 = GA0 * (1 - A1) + GA1 + * B = B0 * A0 * (1 - A1) + B1 * A1 = BA0 * (1 - A1) + BA1 + * A = A0 * (1 - A1) + A1 = A0 * (1 - A1) + A1 + * + * Considering only the red channel, some algebraic transformation is applied to + * make the math quicker to solve. + * + * => ((RA0 / 255.0) * (1.0 - A1 / 255.0) + (RA1 / 255.0)) * 255.0 + * => ((RA0 * 255) - (RA0 * A1) + (RA1 * 255)) / 255 + */ + + // Bottom layer + byte rXa0 = bgraCheckeredPixelData[pixelDataIndex + 2]; + byte gXa0 = bgraCheckeredPixelData[pixelDataIndex + 1]; + byte bXa0 = bgraCheckeredPixelData[pixelDataIndex + 0]; + byte a0 = bgraCheckeredPixelData[pixelDataIndex + 3]; + + // Top layer + byte rXa1 = bgraPixelData[pixelDataIndex + 2]; + byte gXa1 = bgraPixelData[pixelDataIndex + 1]; + byte bXa1 = bgraPixelData[pixelDataIndex + 0]; + byte a1 = bgraPixelData[pixelDataIndex + 3]; + + bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(((bXa0 * 255) - (bXa0 * a1) + (bXa1 * 255)) / 255); + bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(((gXa0 * 255) - (gXa0 * a1) + (gXa1 * 255)) / 255); + bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(((rXa0 * 255) - (rXa0 * a1) + (rXa1 * 255)) / 255); + bgraPixelData[pixelDataIndex + 3] = Convert.ToByte(((a0 * 255) - (a0 * a1) + (a1 * 255)) / 255); + + pixelDataIndex += 4; + } + } + } + + Color GetColor(double channelValue) + { + Color newRgbColor = Colors.White; + + switch (channel) + { + case ColorChannel.Channel1: + { + if (colorRepresentation == ColorRepresentation.Hsva) + { + // Sweep hue + newRgbColor = Helpers.ColorHelper.FromHsv( + Math.Clamp(channelValue, 0.0, 360.0), + baseHsvColor.S, + baseHsvColor.V, + baseHsvColor.A); + } + else + { + // Sweep red + newRgbColor = new Color + { + R = Convert.ToByte(Math.Clamp(channelValue, 0.0, 255.0)), + G = baseRgbColor.G, + B = baseRgbColor.B, + A = baseRgbColor.A + }; + } + + break; + } + + case ColorChannel.Channel2: + { + if (colorRepresentation == ColorRepresentation.Hsva) + { + // Sweep saturation + newRgbColor = Helpers.ColorHelper.FromHsv( + baseHsvColor.H, + Math.Clamp(channelValue, 0.0, 1.0), + baseHsvColor.V, + baseHsvColor.A); + } + else + { + // Sweep green + newRgbColor = new Color + { + R = baseRgbColor.R, + G = Convert.ToByte(Math.Clamp(channelValue, 0.0, 255.0)), + B = baseRgbColor.B, + A = baseRgbColor.A + }; + } + + break; + } + + case ColorChannel.Channel3: + { + if (colorRepresentation == ColorRepresentation.Hsva) + { + // Sweep value + newRgbColor = Helpers.ColorHelper.FromHsv( + baseHsvColor.H, + baseHsvColor.S, + Math.Clamp(channelValue, 0.0, 1.0), + baseHsvColor.A); + } + else + { + // Sweep blue + newRgbColor = new Color + { + R = baseRgbColor.R, + G = baseRgbColor.G, + B = Convert.ToByte(Math.Clamp(channelValue, 0.0, 255.0)), + A = baseRgbColor.A + }; + } + + break; + } + + case ColorChannel.Alpha: + { + if (colorRepresentation == ColorRepresentation.Hsva) + { + // Sweep alpha + newRgbColor = Helpers.ColorHelper.FromHsv( + baseHsvColor.H, + baseHsvColor.S, + baseHsvColor.V, + Math.Clamp(channelValue, 0.0, 1.0)); + } + else + { + // Sweep alpha + newRgbColor = new Color + { + R = baseRgbColor.R, + G = baseRgbColor.G, + B = baseRgbColor.B, + A = Convert.ToByte(Math.Clamp(channelValue, 0.0, 255.0)) + }; + } + + break; + } + } + + return newRgbColor; + } + + return bgraPixelData; + }); + + return bitmap; + } + + /// + /// Generates a new checkered bitmap of the specified size. + /// + /// + /// This is a port and heavy modification of the code here: + /// https://github.com/microsoft/microsoft-ui-xaml/blob/865e4fcc00e8649baeaec1ba7daeca398671aa72/dev/ColorPicker/ColorHelpers.cpp#L363 + /// UWP needs TiledBrush support. + /// + /// The pixel width (X, horizontal) of the checkered bitmap. + /// The pixel height (Y, vertical) of the checkered bitmap. + /// The color of the checker square. + /// A new checkered bitmap of the specified size. + public static async Task CreateCheckeredBitmapAsync( + int width, + int height, + Color checkerColor) + { + // The size of the checker is important. You want it big enough that the grid is clearly discernible. + // However, the squares should be small enough they don't appear unnaturally cut at the edge of backgrounds. + int checkerSize = 4; + + if (width == 0 || height == 0) + { + return null!; + } + + var bitmap = await Task.Run(() => + { + int pixelDataIndex = 0; + byte[] bgraPixelData; + + // Allocate the buffer + // BGRA formatted color channels 1 byte each (4 bytes in a pixel) + bgraPixelData = new byte[width * height * 4]; + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + // We want the checkered pattern to alternate both vertically and horizontally. + // In order to achieve that, we'll toggle visibility of the current pixel on or off + // depending on both its x- and its y-position. If x == CheckerSize, we'll turn visibility off, + // but then if y == CheckerSize, we'll turn it back on. + // The below is a shorthand for the above intent. + bool pixelShouldBeBlank = ((x / checkerSize) + (y / checkerSize)) % 2 == 0 ? true : false; + + // Remember, use BGRA pixel format with pre-multiplied alpha values + if (pixelShouldBeBlank) + { + bgraPixelData[pixelDataIndex + 0] = 0; + bgraPixelData[pixelDataIndex + 1] = 0; + bgraPixelData[pixelDataIndex + 2] = 0; + bgraPixelData[pixelDataIndex + 3] = 0; + } + else + { + bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(checkerColor.B * checkerColor.A / 255); + bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(checkerColor.G * checkerColor.A / 255); + bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(checkerColor.R * checkerColor.A / 255); + bgraPixelData[pixelDataIndex + 3] = checkerColor.A; + } + + pixelDataIndex += 4; + } + } + + return bgraPixelData; + }); + + return bitmap; + } + + /// + /// Converts the given bitmap (in raw BGRA pre-multiplied alpha pixels) into an image brush + /// that can be used in the UI. + /// + /// The bitmap (in raw BGRA pre-multiplied alpha pixels) to convert to a brush. + /// The pixel width of the bitmap. + /// The pixel height of the bitmap. + /// A new ImageBrush. + public static async Task BitmapToBrushAsync( + byte[] bitmap, + int width, + int height) + { + var writableBitmap = new WriteableBitmap(width, height); + using (Stream stream = writableBitmap.PixelBuffer.AsStream()) + { + await stream.WriteAsync(bitmap, 0, bitmap.Length); + } + + var brush = new ImageBrush() + { + ImageSource = writableBitmap, + Stretch = Stretch.None + }; + + return brush; + } + + /// + /// Centralizes code to create a checker brush for a . + /// + /// Border which will have its Background modified. + /// Color to use for transparent checkerboard. + /// Task + public static async Task UpdateBorderBackgroundWithCheckerAsync(Border border, Color color) + { + if (border != null) + { + int width = Convert.ToInt32(border.ActualWidth); + int height = Convert.ToInt32(border.ActualHeight); + + var bitmap = await ColorPickerRenderingHelpers.CreateCheckeredBitmapAsync( + width, + height, + color); + + if (bitmap != null) + { + border.Background = await ColorPickerRenderingHelpers.BitmapToBrushAsync(bitmap, width, height); + } + } + } +} diff --git a/components/ColorPicker/src/ColorPickerSlider.Properties.cs b/components/ColorPicker/src/ColorPickerSlider.Properties.cs new file mode 100644 index 00000000..4baca0ce --- /dev/null +++ b/components/ColorPicker/src/ColorPickerSlider.Properties.cs @@ -0,0 +1,242 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Helpers; +using Windows.UI; + +#if WINAPPSDK +using Colors = Microsoft.UI.Colors; +#else +using Colors = Windows.UI.Colors; +#endif +namespace CommunityToolkit.WinUI.Controls.Primitives; + +/// +public partial class ColorPickerSlider : Slider +{ + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ColorProperty = + DependencyProperty.Register( + nameof(Color), + typeof(Color), + typeof(ColorPickerSlider), + new PropertyMetadata( + Colors.White, + (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets the RGB color represented by the slider. + /// For accuracy use instead. + /// + public Color Color + { + get => (Color)this.GetValue(ColorProperty); + set + { + if (object.Equals(value, this.GetValue(ColorProperty)) == false) + { + this.SetValue(ColorProperty, value); + } + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ColorChannelProperty = + DependencyProperty.Register( + nameof(ColorChannel), + typeof(ColorChannel), + typeof(ColorPickerSlider), + new PropertyMetadata( + ColorChannel.Channel1, + (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets the color channel represented by the slider. + /// + public ColorChannel ColorChannel + { + get => (ColorChannel)this.GetValue(ColorChannelProperty); + set + { + if (object.Equals(value, this.GetValue(ColorChannelProperty)) == false) + { + this.SetValue(ColorChannelProperty, value); + } + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ColorRepresentationProperty = + DependencyProperty.Register( + nameof(ColorRepresentation), + typeof(ColorRepresentation), + typeof(ColorPickerSlider), + new PropertyMetadata( + ColorRepresentation.Rgba, + (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets the color representation used by the slider. + /// + public ColorRepresentation ColorRepresentation + { + get => (ColorRepresentation)this.GetValue(ColorRepresentationProperty); + set + { + if (object.Equals(value, this.GetValue(ColorRepresentationProperty)) == false) + { + this.SetValue(ColorRepresentationProperty, value); + } + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DefaultForegroundProperty = + DependencyProperty.Register( + nameof(DefaultForeground), + typeof(Brush), + typeof(ColorPickerSlider), + new PropertyMetadata( + null, + (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets the default foreground brush to use when the slider background is hardly visible and nearly transparent. + /// Generally, this should be the default Foreground text brush. + /// + public Brush DefaultForeground + { + get => (Brush)this.GetValue(DefaultForegroundProperty); + set + { + if (object.Equals(value, this.GetValue(DefaultForegroundProperty)) == false) + { + this.SetValue(DefaultForegroundProperty, value); + } + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty HsvColorProperty = + DependencyProperty.Register( + nameof(HsvColor), + typeof(HsvColor), + typeof(ColorPickerSlider), + new PropertyMetadata( + Colors.White.ToHsv(), + (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets the HSV color represented by the slider. + /// This is the preferred color property for accuracy. + /// + public HsvColor HsvColor + { + get => (HsvColor)this.GetValue(HsvColorProperty); + set + { + if (object.Equals(value, this.GetValue(HsvColorProperty)) == false) + { + this.SetValue(HsvColorProperty, value); + } + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsAlphaMaxForcedProperty = + DependencyProperty.Register( + nameof(IsAlphaMaxForced), + typeof(bool), + typeof(ColorPickerSlider), + new PropertyMetadata( + true, + (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets a value indicating whether the alpha channel is always forced to maximum for channels + /// other than . + /// This ensures that the background is always visible and never transparent regardless of the actual color. + /// + public bool IsAlphaMaxForced + { + get => (bool)this.GetValue(IsAlphaMaxForcedProperty); + set + { + if (object.Equals(value, this.GetValue(IsAlphaMaxForcedProperty)) == false) + { + this.SetValue(IsAlphaMaxForcedProperty, value); + } + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsAutoUpdatingEnabledProperty = + DependencyProperty.Register( + nameof(IsAutoUpdatingEnabled), + typeof(bool), + typeof(ColorPickerSlider), + new PropertyMetadata( + true, + (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets a value indicating whether automatic background and foreground updates will be + /// calculated when the set color changes. This can be disabled for performance reasons when working with + /// multiple sliders. + /// + public bool IsAutoUpdatingEnabled + { + get => (bool)this.GetValue(IsAutoUpdatingEnabledProperty); + set + { + if (object.Equals(value, this.GetValue(IsAutoUpdatingEnabledProperty)) == false) + { + this.SetValue(IsAutoUpdatingEnabledProperty, value); + } + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsSaturationValueMaxForcedProperty = + DependencyProperty.Register( + nameof(IsSaturationValueMaxForced), + typeof(bool), + typeof(ColorPickerSlider), + new PropertyMetadata( + true, + (s, e) => (s as ColorPickerSlider)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets a value indicating whether the saturation and value channels are always forced to maximum values + /// when in HSVA color representation. Only channel values other than will be changed. + /// This ensures, for example, that the Hue background is always visible and never washed out regardless of the actual color. + /// + public bool IsSaturationValueMaxForced + { + get => (bool)this.GetValue(IsSaturationValueMaxForcedProperty); + set + { + if (object.Equals(value, this.GetValue(IsSaturationValueMaxForcedProperty)) == false) + { + this.SetValue(IsSaturationValueMaxForcedProperty, value); + } + } + } +} diff --git a/components/ColorPicker/src/ColorPickerSlider.cs b/components/ColorPicker/src/ColorPickerSlider.cs new file mode 100644 index 00000000..c9eda3cf --- /dev/null +++ b/components/ColorPicker/src/ColorPickerSlider.cs @@ -0,0 +1,294 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Helpers; +using Windows.UI; + +namespace CommunityToolkit.WinUI.Controls.Primitives; + +/// +/// A slider that represents a single color channel for use in the . +/// +public partial class ColorPickerSlider : Slider +{ + // TODO Combine this with the ColorPicker field or make a property + internal Color CheckerBackgroundColor { get; set; } = Color.FromArgb(0x19, 0x80, 0x80, 0x80); // Overridden later + + private Size oldSize = Size.Empty; + private Size measuredSize = Size.Empty; + private Size cachedSize = Size.Empty; + + /*************************************************************************************** + * + * Constructor/Destructor + * + ***************************************************************************************/ + + /// + /// Initializes a new instance of the class. + /// + public ColorPickerSlider() + : base() + { + this.DefaultStyleKey = typeof(ColorPickerSlider); + } + + /*************************************************************************************** + * + * Methods + * + ***************************************************************************************/ + + /// + /// Update the slider's Foreground and Background brushes based on the current slider state and color. + /// + /// + /// Manually refreshes the background gradient of the slider. + /// This is callable separately for performance reasons. + /// + public void UpdateColors() + { + HsvColor hsvColor = this.HsvColor; + + // Calculate and set the background + this.UpdateBackground(hsvColor); + + // Calculate and set the foreground ensuring contrast with the background + Color rgbColor = Helpers.ColorHelper.FromHsv(hsvColor.H, hsvColor.S, hsvColor.V, hsvColor.A); + Color selectedRgbColor; + double sliderPercent = this.Value / (this.Maximum - this.Minimum); + + if (this.ColorRepresentation == ColorRepresentation.Hsva) + { + if (this.IsAlphaMaxForced && + this.ColorChannel != ColorChannel.Alpha) + { + hsvColor = new HsvColor() + { + H = hsvColor.H, + S = hsvColor.S, + V = hsvColor.V, + A = 1.0 + }; + } + + switch (this.ColorChannel) + { + case ColorChannel.Channel1: + { + var channelValue = Math.Clamp(sliderPercent * 360.0, 0.0, 360.0); + + hsvColor = new HsvColor() + { + H = channelValue, + S = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.S, + V = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.V, + A = hsvColor.A + }; + break; + } + + case ColorChannel.Channel2: + { + var channelValue = Math.Clamp(sliderPercent * 1.0, 0.0, 1.0); + + hsvColor = new HsvColor() + { + H = hsvColor.H, + S = channelValue, + V = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.V, + A = hsvColor.A + }; + break; + } + + case ColorChannel.Channel3: + { + var channelValue = Math.Clamp(sliderPercent * 1.0, 0.0, 1.0); + + hsvColor = new HsvColor() + { + H = hsvColor.H, + S = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.S, + V = channelValue, + A = hsvColor.A + }; + break; + } + } + + selectedRgbColor = Helpers.ColorHelper.FromHsv( + hsvColor.H, + hsvColor.S, + hsvColor.V, + hsvColor.A); + } + else + { + if (this.IsAlphaMaxForced && + this.ColorChannel != ColorChannel.Alpha) + { + rgbColor = new Color() + { + R = rgbColor.R, + G = rgbColor.G, + B = rgbColor.B, + A = 255 + }; + } + + byte channelValue = Convert.ToByte(Math.Clamp(sliderPercent * 255.0, 0.0, 255.0)); + + switch (this.ColorChannel) + { + case ColorChannel.Channel1: + rgbColor = new Color() + { + R = channelValue, + G = rgbColor.G, + B = rgbColor.B, + A = rgbColor.A + }; + break; + case ColorChannel.Channel2: + rgbColor = new Color() + { + R = rgbColor.R, + G = channelValue, + B = rgbColor.B, + A = rgbColor.A + }; + break; + case ColorChannel.Channel3: + rgbColor = new Color() + { + R = rgbColor.R, + G = rgbColor.G, + B = channelValue, + A = rgbColor.A + }; + break; + } + + selectedRgbColor = rgbColor; + } + + var converter = new ContrastBrushConverter(); + 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/components/ColorPicker/src/ColorPickerSlider.xaml b/components/ColorPicker/src/ColorPickerSlider.xaml new file mode 100644 index 00000000..c9cd7890 --- /dev/null +++ b/components/ColorPicker/src/ColorPickerSlider.xaml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/ColorPicker/src/ColorPreviewer.Properties.cs b/components/ColorPicker/src/ColorPreviewer.Properties.cs new file mode 100644 index 00000000..eb6bda8d --- /dev/null +++ b/components/ColorPicker/src/ColorPreviewer.Properties.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Helpers; + +#if WINAPPSDK +using Colors = Microsoft.UI.Colors; +#else +using Colors = Windows.UI.Colors; +#endif + +namespace CommunityToolkit.WinUI.Controls.Primitives; + +/// +public partial class ColorPreviewer +{ + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty HsvColorProperty = + DependencyProperty.Register( + nameof(HsvColor), + typeof(HsvColor), + typeof(ColorPreviewer), + new PropertyMetadata( + Colors.Transparent.ToHsv(), + (s, e) => (s as ColorPreviewer)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets the HSV color represented by the color previewer. + /// This is the preferred color property for accuracy. + /// + public HsvColor HsvColor + { + get => (HsvColor)this.GetValue(HsvColorProperty); + set + { + if (object.Equals(value, this.GetValue(HsvColorProperty)) == false) + { + this.SetValue(HsvColorProperty, value); + } + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ShowAccentColorsProperty = + DependencyProperty.Register( + nameof(ShowAccentColors), + typeof(bool), + typeof(ColorPreviewer), + new PropertyMetadata( + true, + (s, e) => (s as ColorPreviewer)?.OnDependencyPropertyChanged(s, e))); + + /// + /// Gets or sets a value indicating whether accent colors are shown along + /// with the preview color. + /// + public bool ShowAccentColors + { + get => (bool)this.GetValue(ShowAccentColorsProperty); + set + { + if (object.Equals(value, this.GetValue(ShowAccentColorsProperty)) == false) + { + this.SetValue(ShowAccentColorsProperty, value); + } + } + } +} diff --git a/components/ColorPicker/src/ColorPreviewer.cs b/components/ColorPicker/src/ColorPreviewer.cs new file mode 100644 index 00000000..4377958e --- /dev/null +++ b/components/ColorPicker/src/ColorPreviewer.cs @@ -0,0 +1,231 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Globalization; + +namespace CommunityToolkit.WinUI.Controls.Primitives; + +/// +/// Presents a 's preview color with optional accent colors. +/// +[TemplatePart(Name = nameof(ColorPreviewer.CenterCheckeredBackgroundBorder), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPreviewer.LeftCheckeredBackgroundBorder), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPreviewer.RightCheckeredBackgroundBorder), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPreviewer.P1PreviewBorder), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPreviewer.P2PreviewBorder), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPreviewer.N1PreviewBorder), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPreviewer.N2PreviewBorder), Type = typeof(Border))] +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1501:Statement should not be on a single line", Justification = "Inline brackets are used to improve code readability with repeated null checks.")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "Whitespace is used to align code in columns for readability.")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Only template parts start with a capital letter. This differentiates them from other fields.")] +public partial class ColorPreviewer : Control +{ + /// + /// Event for when a color change is requested by user interaction with the previewer. + /// + public event EventHandler ColorChangeRequested; + + private bool eventsConnected = false; + + private Border CenterCheckeredBackgroundBorder; + private Border LeftCheckeredBackgroundBorder; + private Border RightCheckeredBackgroundBorder; + + private Border N1PreviewBorder; + private Border N2PreviewBorder; + private Border P1PreviewBorder; + private Border P2PreviewBorder; + + /*************************************************************************************** + * + * Constructor/Destructor + * + ***************************************************************************************/ + + /// + /// Initializes a new instance of the class. + /// +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public ColorPreviewer() +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + : base() + { + this.DefaultStyleKey = typeof(ColorPreviewer); + } + + /*************************************************************************************** + * + * Methods + * + ***************************************************************************************/ + + /// + /// 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 + { +#pragma warning disable CS0413 +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. + T child = this.GetTemplateChild(childName) as T; +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. +#pragma warning restore CS0413 + + if ((child == null) && isRequired) + { + ThrowArgumentNullException(); + } + + return child!; + + static void ThrowArgumentNullException() => throw new ArgumentNullException(nameof(childName)); + } + + /// + /// 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.CenterCheckeredBackgroundBorder != null) { this.CenterCheckeredBackgroundBorder.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.LeftCheckeredBackgroundBorder != null) { this.LeftCheckeredBackgroundBorder.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.RightCheckeredBackgroundBorder != null) { this.RightCheckeredBackgroundBorder.Loaded += CheckeredBackgroundBorder_Loaded; } + + if (this.N1PreviewBorder != null) { this.N1PreviewBorder.PointerPressed += PreviewBorder_PointerPressed; } + if (this.N2PreviewBorder != null) { this.N2PreviewBorder.PointerPressed += PreviewBorder_PointerPressed; } + if (this.P1PreviewBorder != null) { this.P1PreviewBorder.PointerPressed += PreviewBorder_PointerPressed; } + if (this.P2PreviewBorder != null) { this.P2PreviewBorder.PointerPressed += PreviewBorder_PointerPressed; } + + this.eventsConnected = true; + } + else if (connected == false && this.eventsConnected == true) + { + // Remove all events + if (this.CenterCheckeredBackgroundBorder != null) { this.CenterCheckeredBackgroundBorder.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.LeftCheckeredBackgroundBorder != null) { this.LeftCheckeredBackgroundBorder.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.RightCheckeredBackgroundBorder != null) { this.RightCheckeredBackgroundBorder.Loaded -= CheckeredBackgroundBorder_Loaded; } + + if (this.N1PreviewBorder != null) { this.N1PreviewBorder.PointerPressed -= PreviewBorder_PointerPressed; } + if (this.N2PreviewBorder != null) { this.N2PreviewBorder.PointerPressed -= PreviewBorder_PointerPressed; } + if (this.P1PreviewBorder != null) { this.P1PreviewBorder.PointerPressed -= PreviewBorder_PointerPressed; } + if (this.P2PreviewBorder != null) { this.P2PreviewBorder.PointerPressed -= PreviewBorder_PointerPressed; } + + this.eventsConnected = false; + } + + return; + } + + /*************************************************************************************** + * + * OnEvent Overridable Methods + * + ***************************************************************************************/ + + /// + protected override void OnApplyTemplate() + { + // Remove any existing events present if the control was previously loaded then unloaded + this.ConnectEvents(false); + + this.CenterCheckeredBackgroundBorder = this.GetTemplateChild(nameof(CenterCheckeredBackgroundBorder)); + this.LeftCheckeredBackgroundBorder = this.GetTemplateChild(nameof(LeftCheckeredBackgroundBorder)); + this.RightCheckeredBackgroundBorder = this.GetTemplateChild(nameof(RightCheckeredBackgroundBorder)); + + this.N1PreviewBorder = this.GetTemplateChild(nameof(N1PreviewBorder)); + this.N2PreviewBorder = this.GetTemplateChild(nameof(N2PreviewBorder)); + this.P1PreviewBorder = this.GetTemplateChild(nameof(P1PreviewBorder)); + this.P2PreviewBorder = this.GetTemplateChild(nameof(P2PreviewBorder)); + + // Must connect after controls are resolved + this.ConnectEvents(true); + + base.OnApplyTemplate(); + } + + /// + /// Method called whenever a dependency property value is changed. + /// This may happen through binding directly or a property setter. + /// + /// The sender of the event. + /// The event arguments. + protected virtual void OnDependencyPropertyChanged(object sender, DependencyPropertyChangedEventArgs args) + { + return; + } + + /// + /// Called before the event occurs. + /// + /// The newly requested color. + protected virtual void OnColorChangeRequested(HsvColor color) + { + this.ColorChangeRequested?.Invoke(this, color); + return; + } + + /*************************************************************************************** + * + * Event Handling + * + ***************************************************************************************/ + + /// + /// Event handler to draw checkered backgrounds on-demand as controls are loaded. + /// + private async void CheckeredBackgroundBorder_Loaded(object sender, RoutedEventArgs e) + { + if (sender is Border border) + { + int width = Convert.ToInt32(border.ActualWidth); + int height = Convert.ToInt32(border.ActualHeight); + + var bitmap = await ColorPickerRenderingHelpers.CreateCheckeredBitmapAsync( + width, + height, + ColorPickerRenderingHelpers.CheckerBackgroundColor); + + if (bitmap != null) + { + border.Background = await ColorPickerRenderingHelpers.BitmapToBrushAsync(bitmap, width, height); + } + } + + return; + } + + /// + /// Event handler for when a preview color panel is pressed. + /// This will update the color to the background of the pressed panel. + /// + private void PreviewBorder_PointerPressed(object sender, PointerRoutedEventArgs e) + { + if (sender is Border border) + { + int accentStep = 0; + HsvColor hsvColor = this.HsvColor; + + // Get the value component delta + try + { + if (border.Tag?.ToString() is string tag) + { + accentStep = int.Parse(tag, CultureInfo.InvariantCulture); + } + } + catch { } + + HsvColor newHsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep); + this.OnColorChangeRequested(newHsvColor); + + return; + } + } +} diff --git a/components/ColorPicker/src/ColorPreviewer.xaml b/components/ColorPicker/src/ColorPreviewer.xaml new file mode 100644 index 00000000..3c1947e8 --- /dev/null +++ b/components/ColorPicker/src/ColorPreviewer.xaml @@ -0,0 +1,122 @@ + + + + + diff --git a/components/ColorPicker/src/ColorRepresentation.cs b/components/ColorPicker/src/ColorRepresentation.cs new file mode 100644 index 00000000..d94c47a9 --- /dev/null +++ b/components/ColorPicker/src/ColorRepresentation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Defines how colors are represented. +/// +[EditorBrowsable(EditorBrowsableState.Advanced)] +public enum ColorRepresentation +{ + /// + /// Color is represented by hue, saturation, value and alpha channels. + /// + Hsva, + + /// + /// Color is represented by red, green, blue and alpha channels. + /// + Rgba +} diff --git a/components/ColorPicker/src/CommunityToolkit.WinUI.Controls.ColorPicker.csproj b/components/ColorPicker/src/CommunityToolkit.WinUI.Controls.ColorPicker.csproj new file mode 100644 index 00000000..acf64c70 --- /dev/null +++ b/components/ColorPicker/src/CommunityToolkit.WinUI.Controls.ColorPicker.csproj @@ -0,0 +1,19 @@ + + + ColorPicker + This package contains ColorPicker. + + + CommunityToolkit.WinUI.Controls.ColorPickerRns + + + + + + + + + + + + diff --git a/components/ColorPicker/src/Converters/AccentColorConverter.cs b/components/ColorPicker/src/Converters/AccentColorConverter.cs new file mode 100644 index 00000000..502cfc71 --- /dev/null +++ b/components/ColorPicker/src/Converters/AccentColorConverter.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Helpers; +using System.Globalization; +using Windows.UI; + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Creates an accent color for a base color value. +/// +public class AccentColorConverter : IValueConverter +{ + /// + /// The amount to change the Value channel for each accent color step. + /// + public const double ValueDelta = 0.1; + + /// + /// This does not account for perceptual differences and also does not match with + /// system accent color calculation. + /// + /// + /// Use the HSV representation as it's more perceptual. + /// In most cases only the value is changed by a fixed percentage so the algorithm is reproducible. + /// + /// The base color to calculate the accent from. + /// The number of accent color steps to move. + /// The new accent color. + public static HsvColor GetAccent(HsvColor hsvColor, int accentStep) + { + if (accentStep != 0) + { + double colorValue = hsvColor.V; + colorValue += accentStep * AccentColorConverter.ValueDelta; + colorValue = Math.Round(colorValue, 2); + + return new HsvColor() + { + A = Math.Clamp(hsvColor.A, 0.0, 1.0), + H = Math.Clamp(hsvColor.H, 0.0, 360.0), + S = Math.Clamp(hsvColor.S, 0.0, 1.0), + V = Math.Clamp(colorValue, 0.0, 1.0), + }; + } + else + { + return hsvColor; + } + } + + /// + public object Convert( + object value, + Type targetType, + object parameter, + string language) + { + int accentStep; + Color? rgbColor = null; + HsvColor? hsvColor = null; + + // Get the current color in HSV + if (value is Color valueColor) + { + rgbColor = valueColor; + } + else if (value is HsvColor valueHsvColor) + { + hsvColor = valueHsvColor; + } + else if (value is SolidColorBrush valueBrush) + { + rgbColor = valueBrush.Color; + } + else + { + // Invalid color value provided + return DependencyProperty.UnsetValue; + } + + // Get the value component delta + try + { + accentStep = int.Parse(parameter?.ToString()!, CultureInfo.InvariantCulture); + } + catch + { + // Invalid parameter provided, unable to convert to integer + return DependencyProperty.UnsetValue; + } + + if (hsvColor == null && + rgbColor != null) + { + hsvColor = rgbColor.Value.ToHsv(); + } + + if (hsvColor != null) + { + var hsv = AccentColorConverter.GetAccent(hsvColor.Value, accentStep); + + return Helpers.ColorHelper.FromHsv(hsv.H, hsv.S, hsv.V, hsv.A); + } + else + { + return DependencyProperty.UnsetValue; + } + } + + /// + public object ConvertBack( + object value, + Type targetType, + object parameter, + string language) + { + return DependencyProperty.UnsetValue; + } +} diff --git a/components/ColorPicker/src/Converters/ColorToHexConverter.cs b/components/ColorPicker/src/Converters/ColorToHexConverter.cs new file mode 100644 index 00000000..db6eec76 --- /dev/null +++ b/components/ColorPicker/src/Converters/ColorToHexConverter.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Helpers; +using Windows.UI; + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Converts a color to a hex string and vice versa. +/// +public class ColorToHexConverter : IValueConverter +{ + /// + public object Convert( + object value, + Type targetType, + object parameter, + string language) + { + Color color; + + if (value is Color valueColor) + { + color = valueColor; + } + else if (value is SolidColorBrush valueBrush) + { + color = valueBrush.Color; + } + else + { + // Invalid color value provided + return DependencyProperty.UnsetValue; + } + + string hexColor = color.ToHex().Replace("#", string.Empty); + return hexColor; + } + + /// + public object ConvertBack( + object value, + Type targetType, + object? parameter, + string? language) + { + string hexValue = value.ToString()!; + + if (hexValue.StartsWith("#")) + { + try + { + return hexValue.ToColor(); + } + catch + { + // Invalid hex color value provided + return DependencyProperty.UnsetValue; + } + } + else + { + try + { + return ("#" + hexValue).ToColor(); + } + catch + { + // Invalid hex color value provided + return DependencyProperty.UnsetValue; + } + } + } +} diff --git a/components/ColorPicker/src/Converters/ContrastBrushConverter.cs b/components/ColorPicker/src/Converters/ContrastBrushConverter.cs new file mode 100644 index 00000000..faf4a117 --- /dev/null +++ b/components/ColorPicker/src/Converters/ContrastBrushConverter.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Windows.UI; + +#if WINAPPSDK +using Colors = Microsoft.UI.Colors; +#else +using Colors = Windows.UI.Colors; +#endif + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Gets a color, either black or white, depending on the brightness of the supplied color. +/// +public class ContrastBrushConverter : IValueConverter +{ + /// + /// Gets or sets the alpha channel threshold below which a default color is used instead of black/white. + /// + public byte AlphaThreshold { get; set; } = 128; + + /// + public object Convert( + object value, + Type targetType, + object? parameter, + string? language) + { + Color comparisonColor; + Color? defaultColor = null; + + // Get the changing color to compare against + if (value is Color valueColor) + { + comparisonColor = valueColor; + } + else if (value is SolidColorBrush valueBrush) + { + comparisonColor = valueBrush.Color; + } + else + { + // Invalid color value provided + return DependencyProperty.UnsetValue; + } + + // Get the default color when transparency is high + if (parameter is Color parameterColor) + { + defaultColor = parameterColor; + } + else if (parameter is SolidColorBrush parameterBrush) + { + defaultColor = parameterBrush.Color; + } + + if (comparisonColor.A < AlphaThreshold && + defaultColor.HasValue) + { + // If the transparency is less than 50 %, just use the default brush + // This can commonly be something like the TextControlForeground brush + return new SolidColorBrush(defaultColor.Value); + } + else + { + // Chose a white/black brush based on contrast to the base color + if (this.UseLightContrastColor(comparisonColor)) + { + return new SolidColorBrush(Colors.White); + } + else + { + return new SolidColorBrush(Colors.Black); + } + } + } + + /// + public object ConvertBack( + object value, + Type targetType, + object parameter, + string language) + { + return DependencyProperty.UnsetValue; + } + + /// + /// Determines whether a light or dark contrast color should be used with the given displayed color. + /// + /// + /// This code is using the WinUI algorithm. + /// + private bool UseLightContrastColor(Color displayedColor) + { + // The selection ellipse should be light if and only if the chosen color + // contrasts more with black than it does with white. + // To find how much something contrasts with white, we use the equation + // for relative luminance, which is given by + // + // L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg + // + // where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise } + // + // If L is closer to 1, then the color is closer to white; if it is closer to 0, + // then the color is closer to black. This is based on the fact that the human + // eye perceives green to be much brighter than red, which in turn is perceived to be + // brighter than blue. + // + // If the third dimension is value, then we won't be updating the spectrum's displayed colors, + // so in that case we should use a value of 1 when considering the backdrop + // for the selection ellipse. + double rg = displayedColor.R <= 10 ? displayedColor.R / 3294.0 : Math.Pow((displayedColor.R / 269.0) + 0.0513, 2.4); + double gg = displayedColor.G <= 10 ? displayedColor.G / 3294.0 : Math.Pow((displayedColor.G / 269.0) + 0.0513, 2.4); + double bg = displayedColor.B <= 10 ? displayedColor.B / 3294.0 : Math.Pow((displayedColor.B / 269.0) + 0.0513, 2.4); + + return (0.2126 * rg) + (0.7152 * gg) + (0.0722 * bg) <= 0.5; + } +} diff --git a/components/ColorPicker/src/Dependencies.props b/components/ColorPicker/src/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/ColorPicker/src/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/ColorPicker/src/FluentColorPalette.cs b/components/ColorPicker/src/FluentColorPalette.cs new file mode 100644 index 00000000..18250c02 --- /dev/null +++ b/components/ColorPicker/src/FluentColorPalette.cs @@ -0,0 +1,173 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Color = Windows.UI.Color; // Type can be changed to CoreColor, etc. + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Implements the standard Windows 10 color palette. +/// +public class FluentColorPalette : IColorPalette +{ + /* Values were taken from the Settings App, Personalization > Colors which match with + * https://docs.microsoft.com/en-us/windows/uwp/whats-new/windows-docs-december-2017 + * + * The default ordering and grouping of colors was undesirable so was modified. + * Colors were transposed: the colors in rows within the Settings app became columns here. + * This is because columns in an IColorPalette generally should contain different shades of + * the same color. In the settings app this concept is somewhat loosely reversed. + * The first 'column' ordering, after being transposed, was then reversed so 'red' colors + * were near to each other. + * + * This new ordering most closely follows the Windows standard while: + * + * 1. Keeping colors in a 'spectrum' order + * 2. Keeping like colors next to each both in rows and columns + * (which is unique for the windows palette). + * For example, similar red colors are next to each other in both + * rows within the same column and rows within the column next to it. + * This follows a 'snake-like' pattern as illustrated below. + * 3. A downside of this ordering is colors don't follow strict 'shades' + * as in other palettes. + * + * The colors will be displayed in the below pattern. + * This pattern follows a spectrum while keeping like-colors near to one + * another across both rows and columns. + * + * ┌Red───┐ ┌Blue──┐ ┌Gray──┐ + * │ │ │ │ │ | + * │ │ │ │ │ | + * Yellow └Violet┘ └Green─┘ Brown + */ + private static Color[,] colorChart = new Color[,] + { + { + // Ordering reversed for this section only + Color.FromArgb(255, 255, 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/components/ColorPicker/src/IColorPalette.cs b/components/ColorPicker/src/IColorPalette.cs new file mode 100644 index 00000000..4f8a9cf3 --- /dev/null +++ b/components/ColorPicker/src/IColorPalette.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Windows.UI; + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Interface to define a color palette. +/// +public interface IColorPalette +{ + /// + /// Gets the total number of colors in this palette. + /// A color is not necessarily a single value and may be composed of several shades. + /// + int ColorCount { get; } + + /// + /// Gets the total number of shades for each color in this palette. + /// Shades are usually a variation of the color lightening or darkening it. + /// + int ShadeCount { get; } + + /// + /// Gets a color in the palette by index. + /// + /// The index of the color in the palette. + /// The index must be between zero and . + /// The index of the color shade in the palette. + /// The index must be between zero and . + /// The color at the specified index or an exception. + Color GetColor(int colorIndex, int shadeIndex); +} diff --git a/components/ColorPicker/src/MultiTarget.props b/components/ColorPicker/src/MultiTarget.props new file mode 100644 index 00000000..b11c1942 --- /dev/null +++ b/components/ColorPicker/src/MultiTarget.props @@ -0,0 +1,9 @@ + + + + uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android; + + \ No newline at end of file diff --git a/components/ColorPicker/src/Themes/Generic.xaml b/components/ColorPicker/src/Themes/Generic.xaml new file mode 100644 index 00000000..c1c5ae68 --- /dev/null +++ b/components/ColorPicker/src/Themes/Generic.xaml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/components/ColorPicker/tests/ColorPicker.Tests.projitems b/components/ColorPicker/tests/ColorPicker.Tests.projitems new file mode 100644 index 00000000..032cb653 --- /dev/null +++ b/components/ColorPicker/tests/ColorPicker.Tests.projitems @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 7241E39A-E282-42BF-A260-30BF986D876D + + + ColorPickerExperiment.Tests + + + + + ExampleColorPickerTestPage.xaml + + + + + Designer + MSBuild:Compile + + + \ No newline at end of file diff --git a/components/ColorPicker/tests/ColorPicker.Tests.shproj b/components/ColorPicker/tests/ColorPicker.Tests.shproj new file mode 100644 index 00000000..df238853 --- /dev/null +++ b/components/ColorPicker/tests/ColorPicker.Tests.shproj @@ -0,0 +1,13 @@ + + + + 7241E39A-E282-42BF-A260-30BF986D876D + 14.0 + + + + + + + + diff --git a/components/ColorPicker/tests/ExampleColorPickerTestClass.cs b/components/ColorPicker/tests/ExampleColorPickerTestClass.cs new file mode 100644 index 00000000..01e03403 --- /dev/null +++ b/components/ColorPicker/tests/ExampleColorPickerTestClass.cs @@ -0,0 +1,135 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Tooling.TestGen; +using CommunityToolkit.Tests; +using CommunityToolkit.WinUI.Controls; +using ColorPicker = CommunityToolkit.WinUI.Controls.ColorPicker; + +namespace ColorPickerExperiment.Tests; + +[TestClass] +public partial class ExampleColorPickerTestClass : VisualUITestBase +{ + // If you don't need access to UI objects directly or async code, use this pattern. + [TestMethod] + public void SimpleSynchronousExampleTest() + { + var assembly = typeof(ColorPicker).Assembly; + var type = assembly.GetType(typeof(ColorPicker).FullName ?? string.Empty); + + Assert.IsNotNull(type, "Could not find ColorPicker type."); + Assert.AreEqual(typeof(ColorPicker), type, "Type of ColorPicker does not match expected type."); + } + + // If you don't need access to UI objects directly, use this pattern. + [TestMethod] + public async Task SimpleAsyncExampleTest() + { + await Task.Delay(250); + + Assert.IsTrue(true); + } + + // Example that shows how to check for exception throwing. + [TestMethod] + public void SimpleExceptionCheckTest() + { + // If you need to check exceptions occur for invalid inputs, etc... + // Use Assert.ThrowsException to limit the scope to where you expect the error to occur. + // Otherwise, using the ExpectedException attribute could swallow or + // catch other issues in setup code. + Assert.ThrowsException(() => throw new NotImplementedException()); + } + + // The UIThreadTestMethod automatically dispatches to the UI for us to work with UI objects. + [UIThreadTestMethod] + public void SimpleUIAttributeExampleTest() + { + var component = new ColorPicker(); + Assert.IsNotNull(component); + } + + // The UIThreadTestMethod can also easily grab a XAML Page for us by passing its type as a parameter. + // This lets us actually test a control as it would behave within an actual application. + // The page will already be loaded by the time your test is called. + [UIThreadTestMethod] + public void SimpleUIExamplePageTest(ExampleColorPickerTestPage page) + { + // You can use the Toolkit Visual Tree helpers here to find the component by type or name: + var component = page.FindDescendant(); + + Assert.IsNotNull(component); + + var componentByName = page.FindDescendant("ColorPickerControl"); + + Assert.IsNotNull(componentByName); + } + + // You can still do async work with a UIThreadTestMethod as well. + [UIThreadTestMethod] + public async Task SimpleAsyncUIExamplePageTest(ExampleColorPickerTestPage page) + { + // This helper can be used to wait for a rendering pass to complete. + // Note, this is already done by loading a Page with the [UIThreadTestMethod] helper. + await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { }); + + var component = page.FindDescendant(); + + Assert.IsNotNull(component); + } + + //// ----------------------------- ADVANCED TEST SCENARIOS ----------------------------- + + // If you need to use DataRow, you can use this pattern with the UI dispatch still. + // Otherwise, checkout the UIThreadTestMethod attribute above. + // See https://github.com/CommunityToolkit/Labs-Windows/issues/186 + [TestMethod] + public async Task ComplexAsyncUIExampleTest() + { + await EnqueueAsync(() => + { + var component = new ColorPicker(); + Assert.IsNotNull(component); + }); + } + + // If you want to load other content not within a XAML page using the UIThreadTestMethod above. + // Then you can do that using the Load/UnloadTestContentAsync methods. + [TestMethod] + public async Task ComplexAsyncLoadUIExampleTest() + { + await EnqueueAsync(async () => + { + var component = new ColorPicker(); + Assert.IsNotNull(component); + Assert.IsFalse(component.IsLoaded); + + await LoadTestContentAsync(component); + + Assert.IsTrue(component.IsLoaded); + + await UnloadTestContentAsync(component); + + Assert.IsFalse(component.IsLoaded); + }); + } + + // You can still use the UIThreadTestMethod to remove the extra layer for the dispatcher as well: + [UIThreadTestMethod] + public async Task ComplexAsyncLoadUIExampleWithoutDispatcherTest() + { + var component = new ColorPicker(); + Assert.IsNotNull(component); + Assert.IsFalse(component.IsLoaded); + + await LoadTestContentAsync(component); + + Assert.IsTrue(component.IsLoaded); + + await UnloadTestContentAsync(component); + + Assert.IsFalse(component.IsLoaded); + } +} diff --git a/components/ColorPicker/tests/ExampleColorPickerTestPage.xaml b/components/ColorPicker/tests/ExampleColorPickerTestPage.xaml new file mode 100644 index 00000000..5d68f92d --- /dev/null +++ b/components/ColorPicker/tests/ExampleColorPickerTestPage.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/components/ColorPicker/tests/ExampleColorPickerTestPage.xaml.cs b/components/ColorPicker/tests/ExampleColorPickerTestPage.xaml.cs new file mode 100644 index 00000000..851294df --- /dev/null +++ b/components/ColorPicker/tests/ExampleColorPickerTestPage.xaml.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace ColorPickerExperiment.Tests; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class ExampleColorPickerTestPage : Page +{ + public ExampleColorPickerTestPage() + { + this.InitializeComponent(); + } +} From be939c7c49fa39d62b1f7c1f2c697b5382f4b943 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Wed, 20 Sep 2023 13:47:50 +0200 Subject: [PATCH 02/18] Use default GetTemplateChild --- components/ColorPicker/src/ColorPicker.cs | 104 +++++++++---------- components/ColorPicker/src/ColorPreviewer.cs | 44 ++++---- 2 files changed, 74 insertions(+), 74 deletions(-) diff --git a/components/ColorPicker/src/ColorPicker.cs b/components/ColorPicker/src/ColorPicker.cs index cdd0a1a2..ff4e84f6 100644 --- a/components/ColorPicker/src/ColorPicker.cs +++ b/components/ColorPicker/src/ColorPicker.cs @@ -173,38 +173,38 @@ protected override void OnApplyTemplate() // We need to disconnect old events first this.ConnectEvents(false); - this.ColorPanelSelector = this.GetTemplateChild(nameof(ColorPanelSelector)); - - 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.Channel1NumberBox = this.GetTemplateChild(nameof(Channel1NumberBox)); - this.Channel2NumberBox = this.GetTemplateChild(nameof(Channel2NumberBox)); - this.Channel3NumberBox = this.GetTemplateChild(nameof(Channel3NumberBox)); - this.AlphaChannelNumberBox = this.GetTemplateChild(nameof(AlphaChannelNumberBox)); - - 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.ColorPreviewer = this.GetTemplateChild(nameof(ColorPreviewer)); - - 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)); + this.ColorPanelSelector = (ListBox)GetTemplateChild(nameof(ColorPanelSelector)); + + this.ColorSpectrumControl = (ColorSpectrum)GetTemplateChild(nameof(ColorSpectrumControl)); + this.ColorSpectrumAlphaSlider = (ColorPickerSlider)this.GetTemplateChild(nameof(ColorSpectrumAlphaSlider)); + this.ColorSpectrumThirdDimensionSlider = (ColorPickerSlider)this.GetTemplateChild(nameof(ColorSpectrumThirdDimensionSlider)); + + this.HexInputTextBox = (TextBox)this.GetTemplateChild(nameof(HexInputTextBox)); + this.HsvToggleButton = (ToggleButton)this.GetTemplateChild(nameof(HsvToggleButton)); + this.RgbToggleButton = (ToggleButton)this.GetTemplateChild(nameof(RgbToggleButton)); + + this.Channel1NumberBox = (NumberBox)this.GetTemplateChild(nameof(Channel1NumberBox)); + this.Channel2NumberBox = (NumberBox)this.GetTemplateChild(nameof(Channel2NumberBox)); + this.Channel3NumberBox = (NumberBox)this.GetTemplateChild(nameof(Channel3NumberBox)); + this.AlphaChannelNumberBox = (NumberBox)this.GetTemplateChild(nameof(AlphaChannelNumberBox)); + + this.Channel1Slider = (ColorPickerSlider)this.GetTemplateChild(nameof(Channel1Slider)); + this.Channel2Slider = (ColorPickerSlider)this.GetTemplateChild(nameof(Channel2Slider)); + this.Channel3Slider = (ColorPickerSlider)this.GetTemplateChild(nameof(Channel3Slider)); + this.AlphaChannelSlider = (ColorPickerSlider)this.GetTemplateChild(nameof(AlphaChannelSlider)); + + this.ColorPreviewer = (ColorPreviewer)this.GetTemplateChild(nameof(ColorPreviewer)); + + this.CheckeredBackground1Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground1Border)); + this.CheckeredBackground2Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground2Border)); + this.CheckeredBackground3Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground3Border)); + this.CheckeredBackground4Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground4Border)); + this.CheckeredBackground5Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground5Border)); + this.CheckeredBackground6Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground6Border)); + this.CheckeredBackground7Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground7Border)); + this.CheckeredBackground8Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground8Border)); + this.CheckeredBackground9Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground9Border)); + this.CheckeredBackground10Border = (Border)this.GetTemplateChild(nameof(CheckeredBackground10Border)); // Must connect after controls are resolved this.ConnectEvents(true); @@ -223,26 +223,26 @@ protected override void OnApplyTemplate() /// 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 - { -#pragma warning disable CS0413 -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. - T child = this.GetTemplateChild(childName) as T; -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. -#pragma warning restore CS0413 - - if ((child == null) && isRequired) - { - ThrowArgumentNullException(); - } - -#pragma warning disable CS8603 // Possible null reference return. - return child; -#pragma warning restore CS8603 // Possible null reference return. - - static void ThrowArgumentNullException() => throw new ArgumentNullException(nameof(childName)); - } +// private T GetTemplateChild(string childName, bool isRequired = false) +// where T : DependencyObject +// { +//#pragma warning disable CS0413 +//#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. +// T child = this.GetTemplateChild(childName) as T; +//#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. +//#pragma warning restore CS0413 + +// if ((child == null) && isRequired) +// { +// ThrowArgumentNullException(); +// } + +//#pragma warning disable CS8603 // Possible null reference return. +// return child; +//#pragma warning restore CS8603 // Possible null reference return. + +// static void ThrowArgumentNullException() => throw new ArgumentNullException(nameof(childName)); +// } /// /// Connects or disconnects all dependency property callbacks. diff --git a/components/ColorPicker/src/ColorPreviewer.cs b/components/ColorPicker/src/ColorPreviewer.cs index 4377958e..91394d4b 100644 --- a/components/ColorPicker/src/ColorPreviewer.cs +++ b/components/ColorPicker/src/ColorPreviewer.cs @@ -66,24 +66,24 @@ public ColorPreviewer() /// 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 - { -#pragma warning disable CS0413 -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. - T child = this.GetTemplateChild(childName) as T; -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. -#pragma warning restore CS0413 +// private T GetTemplateChild(string childName, bool isRequired = false) +// where T : DependencyObject +// { +//#pragma warning disable CS0413 +//#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. +// T child = this.GetTemplateChild(childName) as T; +//#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. +//#pragma warning restore CS0413 - if ((child == null) && isRequired) - { - ThrowArgumentNullException(); - } +// if ((child == null) && isRequired) +// { +// ThrowArgumentNullException(); +// } - return child!; +// return child!; - static void ThrowArgumentNullException() => throw new ArgumentNullException(nameof(childName)); - } +// static void ThrowArgumentNullException() => throw new ArgumentNullException(nameof(childName)); +// } /// /// Connects or disconnects all control event handlers. @@ -135,14 +135,14 @@ protected override void OnApplyTemplate() // Remove any existing events present if the control was previously loaded then unloaded this.ConnectEvents(false); - this.CenterCheckeredBackgroundBorder = this.GetTemplateChild(nameof(CenterCheckeredBackgroundBorder)); - this.LeftCheckeredBackgroundBorder = this.GetTemplateChild(nameof(LeftCheckeredBackgroundBorder)); - this.RightCheckeredBackgroundBorder = this.GetTemplateChild(nameof(RightCheckeredBackgroundBorder)); + this.CenterCheckeredBackgroundBorder = (Border)this.GetTemplateChild(nameof(CenterCheckeredBackgroundBorder)); + this.LeftCheckeredBackgroundBorder = (Border)this.GetTemplateChild(nameof(LeftCheckeredBackgroundBorder)); + this.RightCheckeredBackgroundBorder = (Border)this.GetTemplateChild(nameof(RightCheckeredBackgroundBorder)); - 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.N1PreviewBorder = (Border)this.GetTemplateChild(nameof(N1PreviewBorder)); + this.N2PreviewBorder = (Border)this.GetTemplateChild(nameof(N2PreviewBorder)); + this.P1PreviewBorder = (Border)this.GetTemplateChild(nameof(P1PreviewBorder)); + this.P2PreviewBorder = (Border)this.GetTemplateChild(nameof(P2PreviewBorder)); // Must connect after controls are resolved this.ConnectEvents(true); From 98092755f18aa2eb4f4d355a41f06954fa206c69 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Wed, 20 Sep 2023 20:24:01 +0200 Subject: [PATCH 03/18] Visual updates --- .../samples/ColorPickerCustomSample.xaml | 20 +- .../samples/ColorPickerCustomSample.xaml.cs | 8 + components/ColorPicker/src/ColorPicker.cs | 62 +- components/ColorPicker/src/ColorPicker.xaml | 1347 ++++------------- .../ColorPicker/src/ColorPickerSlider.cs | 1 - .../ColorPicker/src/ColorPickerSlider.xaml | 156 +- .../ColorPicker/src/ColorPreviewer.xaml | 40 +- ...yToolkit.WinUI.Controls.ColorPicker.csproj | 3 +- .../ColorPicker/src/FluentColorPalette.cs | 96 +- 9 files changed, 508 insertions(+), 1225 deletions(-) diff --git a/components/ColorPicker/samples/ColorPickerCustomSample.xaml b/components/ColorPicker/samples/ColorPickerCustomSample.xaml index 3f873cd7..17fc225c 100644 --- a/components/ColorPicker/samples/ColorPickerCustomSample.xaml +++ b/components/ColorPicker/samples/ColorPickerCustomSample.xaml @@ -1,4 +1,4 @@ - + - - + + + diff --git a/components/ColorPicker/samples/ColorPickerCustomSample.xaml.cs b/components/ColorPicker/samples/ColorPickerCustomSample.xaml.cs index 81c45e2f..65f8f997 100644 --- a/components/ColorPicker/samples/ColorPickerCustomSample.xaml.cs +++ b/components/ColorPicker/samples/ColorPickerCustomSample.xaml.cs @@ -9,6 +9,14 @@ namespace ColorPickerExperiment.Samples; /// /// An example sample page of a custom control inheriting from Panel. /// +[ToolkitSampleBoolOption("AccentColors", true, Title = "ShowAccentColors")] +[ToolkitSampleBoolOption("ColorPalette", true, Title = "IsColorPaletteVisible")] +[ToolkitSampleBoolOption("AlphaSlider", true, Title = "IsAlphaSliderVisible")] +[ToolkitSampleBoolOption("ColorChannel", true, Title = "IsColorChannelTextInputVisible")] +[ToolkitSampleBoolOption("ColorSlider", true, Title = "IsColorSliderVisible")] +[ToolkitSampleBoolOption("ColorSpectrum", true, Title = "IsColorSpectrumVisible")] + + [ToolkitSampleTextOption("TitleText", "This is a title", Title = "Input the text")] [ToolkitSampleMultiChoiceOption("LayoutOrientation", "Horizontal", "Vertical", Title = "Orientation")] diff --git a/components/ColorPicker/src/ColorPicker.cs b/components/ColorPicker/src/ColorPicker.cs index ff4e84f6..0f7a802e 100644 --- a/components/ColorPicker/src/ColorPicker.cs +++ b/components/ColorPicker/src/ColorPicker.cs @@ -41,13 +41,15 @@ namespace CommunityToolkit.WinUI.Controls; [TemplatePart(Name = nameof(ColorPicker.CheckeredBackground8Border), Type = typeof(Border))] [TemplatePart(Name = nameof(ColorPicker.CheckeredBackground9Border), Type = typeof(Border))] [TemplatePart(Name = nameof(ColorPicker.CheckeredBackground10Border), Type = typeof(Border))] -[TemplatePart(Name = nameof(ColorPicker.ColorPanelSelector), Type = typeof(ListBox))] +[TemplatePart(Name = nameof(ColorPicker.ColorPanelSelector), Type = typeof(Segmented))] [TemplatePart(Name = nameof(ColorPicker.ColorSpectrumControl), Type = typeof(ColorSpectrum))] [TemplatePart(Name = nameof(ColorPicker.ColorSpectrumAlphaSlider), Type = typeof(ColorPickerSlider))] [TemplatePart(Name = nameof(ColorPicker.ColorSpectrumThirdDimensionSlider), Type = typeof(ColorPickerSlider))] [TemplatePart(Name = nameof(ColorPicker.HexInputTextBox), Type = typeof(TextBox))] -[TemplatePart(Name = nameof(ColorPicker.HsvToggleButton), Type = typeof(ToggleButton))] -[TemplatePart(Name = nameof(ColorPicker.RgbToggleButton), Type = typeof(ToggleButton))] +[TemplatePart(Name = nameof(ColorPicker.ColorModeComboBox), Type = typeof(ComboBox))] + + + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1501:Statement should not be on a single line", Justification = "Inline brackets are used to improve code readability with repeated null checks.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "Whitespace is used to align code in columns for readability.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Only template parts start with a capital letter. This differentiates them from other fields.")] @@ -74,13 +76,12 @@ public partial class ColorPicker : Microsoft.UI.Xaml.Controls.ColorPicker private Color? updatedRgbColor = null; private DispatcherQueueTimer? dispatcherQueueTimer = null; - private ListBox ColorPanelSelector; + private Segmented ColorPanelSelector; private ColorSpectrum ColorSpectrumControl; private ColorPickerSlider ColorSpectrumAlphaSlider; private ColorPickerSlider ColorSpectrumThirdDimensionSlider; private TextBox HexInputTextBox; - private ToggleButton? HsvToggleButton; - private ToggleButton? RgbToggleButton; + private ComboBox ColorModeComboBox; private NumberBox Channel1NumberBox; private NumberBox Channel2NumberBox; @@ -173,15 +174,14 @@ protected override void OnApplyTemplate() // We need to disconnect old events first this.ConnectEvents(false); - this.ColorPanelSelector = (ListBox)GetTemplateChild(nameof(ColorPanelSelector)); + this.ColorPanelSelector = (Segmented)GetTemplateChild(nameof(ColorPanelSelector)); this.ColorSpectrumControl = (ColorSpectrum)GetTemplateChild(nameof(ColorSpectrumControl)); this.ColorSpectrumAlphaSlider = (ColorPickerSlider)this.GetTemplateChild(nameof(ColorSpectrumAlphaSlider)); this.ColorSpectrumThirdDimensionSlider = (ColorPickerSlider)this.GetTemplateChild(nameof(ColorSpectrumThirdDimensionSlider)); this.HexInputTextBox = (TextBox)this.GetTemplateChild(nameof(HexInputTextBox)); - this.HsvToggleButton = (ToggleButton)this.GetTemplateChild(nameof(HsvToggleButton)); - this.RgbToggleButton = (ToggleButton)this.GetTemplateChild(nameof(RgbToggleButton)); + this.ColorModeComboBox = (ComboBox)this.GetTemplateChild(nameof(ColorModeComboBox)); this.Channel1NumberBox = (NumberBox)this.GetTemplateChild(nameof(Channel1NumberBox)); this.Channel2NumberBox = (NumberBox)this.GetTemplateChild(nameof(Channel2NumberBox)); @@ -284,10 +284,7 @@ private void ConnectEvents(bool connected) 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.ColorModeComboBox != null) { this.ColorModeComboBox.SelectionChanged += ColorModeComboBox_SelectionChanged; } if (this.Channel1NumberBox != null) { this.Channel1NumberBox.ValueChanged += ChannelNumberBox_ValueChanged; } if (this.Channel2NumberBox != null) { this.Channel2NumberBox.ValueChanged += ChannelNumberBox_ValueChanged; } @@ -331,10 +328,7 @@ private void ConnectEvents(bool connected) 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.ColorModeComboBox != null) { this.ColorModeComboBox.SelectionChanged -= ColorModeComboBox_SelectionChanged; } if (this.Channel1NumberBox != null) { this.Channel1NumberBox.ValueChanged -= ChannelNumberBox_ValueChanged; } if (this.Channel2NumberBox != null) { this.Channel2NumberBox.ValueChanged -= ChannelNumberBox_ValueChanged; } @@ -374,6 +368,7 @@ private void ConnectEvents(bool connected) return; } + /// /// Updates all visual states based on current control properties. /// @@ -394,8 +389,9 @@ 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) + + if (this.ColorModeComboBox != null && + this.ColorModeComboBox.SelectedIndex == 1) { return ColorRepresentation.Hsva; } @@ -429,31 +425,11 @@ private void SetActiveColorRepresentation(ColorRepresentation? colorRepresentati // 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; - } + this.ColorModeComboBox.SelectedIndex = 1; } 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.ColorModeComboBox.SelectedIndex = 0; } this.UpdateVisualState(false); @@ -1343,9 +1319,9 @@ private void ColorSpectrum_GotFocus(object sender, RoutedEventArgs e) /// Event handler for when the selected color representation changes. /// This will convert between RGB and HSV. /// - private void ColorRepToggleButton_CheckedUnchecked(object sender, RoutedEventArgs e) + private void ColorModeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { - if (object.ReferenceEquals(sender, this.HsvToggleButton)) + if (this.ColorModeComboBox.SelectedIndex == 1) { this.SetActiveColorRepresentation(ColorRepresentation.Hsva); } diff --git a/components/ColorPicker/src/ColorPicker.xaml b/components/ColorPicker/src/ColorPicker.xaml index eac639eb..456b650f 100644 --- a/components/ColorPicker/src/ColorPicker.xaml +++ b/components/ColorPicker/src/ColorPicker.xaml @@ -3,32 +3,25 @@ xmlns:controls="using:CommunityToolkit.WinUI.Controls" xmlns:converters="using:CommunityToolkit.WinUI.Converters" xmlns:localconverters="using:CommunityToolkit.WinUI.Controls" + xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:primitives="using:CommunityToolkit.WinUI.Controls.Primitives" xmlns:ui="using:CommunityToolkit.WinUI" - xmlns:muxc="using:Microsoft.UI.Xaml.Controls" + xmlns:win="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:winuiprimitives="using:Microsoft.UI.Xaml.Controls.Primitives"> - - - Transparent - - - - - #70F5F5F5 - - - + + + + - + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" + RowSpacing="12"> - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -419,7 +73,7 @@ - + @@ -427,7 +81,7 @@ - + @@ -479,7 +133,7 @@ - + @@ -529,723 +183,282 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + RGB + HSV + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - - - - + + + + + + + + - + + + + + + - - diff --git a/components/ColorPicker/src/ColorPickerSlider.cs b/components/ColorPicker/src/ColorPickerSlider.cs index c9eda3cf..1bff1d26 100644 --- a/components/ColorPicker/src/ColorPickerSlider.cs +++ b/components/ColorPicker/src/ColorPickerSlider.cs @@ -175,7 +175,6 @@ public void UpdateColors() } var converter = new ContrastBrushConverter(); - this.Foreground = converter.Convert(selectedRgbColor, typeof(Brush), this.DefaultForeground, null) as Brush; return; } diff --git a/components/ColorPicker/src/ColorPickerSlider.xaml b/components/ColorPicker/src/ColorPickerSlider.xaml index c9cd7890..1e074703 100644 --- a/components/ColorPicker/src/ColorPickerSlider.xaml +++ b/components/ColorPicker/src/ColorPickerSlider.xaml @@ -5,6 +5,114 @@ + - @@ -46,16 +136,12 @@ - - - - @@ -84,7 +170,7 @@ Background="{ThemeResource SliderContainerBackground}" Control.IsTemplateFocusTarget="True"> + MinHeight="18"> @@ -101,21 +187,20 @@ + RadiusX="6" + RadiusY="6" /> + RadiusX="6" + RadiusY="6" /> @@ -142,10 +227,11 @@ + RadiusX="6" + RadiusY="6" /> + xmlns:ui="using:CommunityToolkit.WinUI"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RGB + HSV + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - + + - - - - - + + + - - - - - - RGB - HSV - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Visible + + + + + + + + + + + + + + + + + + + + + + + + + + Foreground="{Binding PlaceholderForeground, RelativeSource={RelativeSource TemplatedParent}, TargetNullValue={ThemeResource TextControlPlaceholderForeground}}" + IsHitTestVisible="False" + Text="{TemplateBinding PlaceholderText}" /> - - - - - - - - - - - - - + Padding="{ThemeResource HelperButtonThemePadding}" + VerticalAlignment="Stretch" + AutomationProperties.AccessibilityView="Raw" + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" + FontSize="{TemplateBinding FontSize}" + IsTabStop="False" + Style="{StaticResource DeleteButtonStyle}" + Visibility="Collapsed" /> + + + + + + + + + + + + + diff --git a/components/ColorPicker/src/ColorPickerButton.cs b/components/ColorPicker/src/ColorPickerButton.cs index 5b8d80d4..e1a2e0f5 100644 --- a/components/ColorPicker/src/ColorPickerButton.cs +++ b/components/ColorPicker/src/ColorPickerButton.cs @@ -36,7 +36,7 @@ public Style ColorPickerStyle /// /// Identifies the dependency property. /// - public static readonly DependencyProperty ColorPickerStyleProperty = DependencyProperty.Register("ColorPickerStyle", typeof(Style), typeof(ColorPickerButton), new PropertyMetadata(default(Style))); + public static readonly DependencyProperty ColorPickerStyleProperty = DependencyProperty.Register(nameof(ColorPickerStyle), typeof(Style), typeof(ColorPickerButton), new PropertyMetadata(default(Style))); /// /// Gets or sets the for the used within the of the . @@ -57,7 +57,7 @@ public Style FlyoutPresenterStyle /// /// Identifies the dependency property. /// - public static readonly DependencyProperty FlyoutPresenterStyleProperty = DependencyProperty.Register("FlyoutPresenterStyle", typeof(Style), typeof(ColorPickerButton), new PropertyMetadata(default(Style))); + public static readonly DependencyProperty FlyoutPresenterStyleProperty = DependencyProperty.Register(nameof(FlyoutPresenterStyle), typeof(Style), typeof(ColorPickerButton), new PropertyMetadata(default(Style))); #pragma warning disable CS0419 // Ambiguous reference in cref attribute /// @@ -89,6 +89,9 @@ public ColorPickerButton() #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. { this.DefaultStyleKey = typeof(ColorPickerButton); + + // Workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/3502 + this.DefaultStyleResourceUri = new Uri("ms-appx:///CommunityToolkit.WinUI.Controls.ColorPicker/Themes/Generic.xaml"); } /// diff --git a/components/ColorPicker/src/ColorPickerButton.xaml b/components/ColorPicker/src/ColorPickerButton.xaml index 9a323876..10126cc5 100644 --- a/components/ColorPicker/src/ColorPickerButton.xaml +++ b/components/ColorPicker/src/ColorPickerButton.xaml @@ -1,163 +1,187 @@ + xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals" + xmlns:controls="using:CommunityToolkit.WinUI.Controls" + xmlns:muxc="using:Microsoft.UI.Xaml.Controls"> - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + - - + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + - - - + + + - - \ No newline at end of file + diff --git a/components/ColorPicker/src/ColorPreviewer.cs b/components/ColorPicker/src/ColorPreviewer.cs index 91394d4b..4519327e 100644 --- a/components/ColorPicker/src/ColorPreviewer.cs +++ b/components/ColorPicker/src/ColorPreviewer.cs @@ -9,9 +9,7 @@ namespace CommunityToolkit.WinUI.Controls.Primitives; /// /// Presents a 's preview color with optional accent colors. /// -[TemplatePart(Name = nameof(ColorPreviewer.CenterCheckeredBackgroundBorder), Type = typeof(Border))] -[TemplatePart(Name = nameof(ColorPreviewer.LeftCheckeredBackgroundBorder), Type = typeof(Border))] -[TemplatePart(Name = nameof(ColorPreviewer.RightCheckeredBackgroundBorder), Type = typeof(Border))] +[TemplatePart(Name = nameof(ColorPreviewer.CheckeredBackgroundBorder), Type = typeof(Border))] [TemplatePart(Name = nameof(ColorPreviewer.P1PreviewBorder), Type = typeof(Border))] [TemplatePart(Name = nameof(ColorPreviewer.P2PreviewBorder), Type = typeof(Border))] [TemplatePart(Name = nameof(ColorPreviewer.N1PreviewBorder), Type = typeof(Border))] @@ -28,9 +26,7 @@ public partial class ColorPreviewer : Control private bool eventsConnected = false; - private Border CenterCheckeredBackgroundBorder; - private Border LeftCheckeredBackgroundBorder; - private Border RightCheckeredBackgroundBorder; + private Border CheckeredBackgroundBorder; private Border N1PreviewBorder; private Border N2PreviewBorder; @@ -94,9 +90,7 @@ private void ConnectEvents(bool connected) if (connected == true && this.eventsConnected == false) { // Add all events - if (this.CenterCheckeredBackgroundBorder != null) { this.CenterCheckeredBackgroundBorder.Loaded += CheckeredBackgroundBorder_Loaded; } - if (this.LeftCheckeredBackgroundBorder != null) { this.LeftCheckeredBackgroundBorder.Loaded += CheckeredBackgroundBorder_Loaded; } - if (this.RightCheckeredBackgroundBorder != null) { this.RightCheckeredBackgroundBorder.Loaded += CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackgroundBorder != null) { this.CheckeredBackgroundBorder.Loaded += CheckeredBackgroundBorder_Loaded; } if (this.N1PreviewBorder != null) { this.N1PreviewBorder.PointerPressed += PreviewBorder_PointerPressed; } if (this.N2PreviewBorder != null) { this.N2PreviewBorder.PointerPressed += PreviewBorder_PointerPressed; } @@ -108,9 +102,7 @@ private void ConnectEvents(bool connected) else if (connected == false && this.eventsConnected == true) { // Remove all events - if (this.CenterCheckeredBackgroundBorder != null) { this.CenterCheckeredBackgroundBorder.Loaded -= CheckeredBackgroundBorder_Loaded; } - if (this.LeftCheckeredBackgroundBorder != null) { this.LeftCheckeredBackgroundBorder.Loaded -= CheckeredBackgroundBorder_Loaded; } - if (this.RightCheckeredBackgroundBorder != null) { this.RightCheckeredBackgroundBorder.Loaded -= CheckeredBackgroundBorder_Loaded; } + if (this.CheckeredBackgroundBorder != null) { this.CheckeredBackgroundBorder.Loaded -= CheckeredBackgroundBorder_Loaded; } if (this.N1PreviewBorder != null) { this.N1PreviewBorder.PointerPressed -= PreviewBorder_PointerPressed; } if (this.N2PreviewBorder != null) { this.N2PreviewBorder.PointerPressed -= PreviewBorder_PointerPressed; } @@ -135,9 +127,7 @@ protected override void OnApplyTemplate() // Remove any existing events present if the control was previously loaded then unloaded this.ConnectEvents(false); - this.CenterCheckeredBackgroundBorder = (Border)this.GetTemplateChild(nameof(CenterCheckeredBackgroundBorder)); - this.LeftCheckeredBackgroundBorder = (Border)this.GetTemplateChild(nameof(LeftCheckeredBackgroundBorder)); - this.RightCheckeredBackgroundBorder = (Border)this.GetTemplateChild(nameof(RightCheckeredBackgroundBorder)); + this.CheckeredBackgroundBorder = (Border)this.GetTemplateChild(nameof(CheckeredBackgroundBorder)); this.N1PreviewBorder = (Border)this.GetTemplateChild(nameof(N1PreviewBorder)); this.N2PreviewBorder = (Border)this.GetTemplateChild(nameof(N2PreviewBorder)); @@ -158,6 +148,7 @@ protected override void OnApplyTemplate() /// The event arguments. protected virtual void OnDependencyPropertyChanged(object sender, DependencyPropertyChangedEventArgs args) { + VisualStateManager.GoToState(this, ShowAccentColors ? "AccentColorsVisible" : "AccentColorsCollapsed", true); return; } diff --git a/components/ColorPicker/src/ColorPreviewer.xaml b/components/ColorPicker/src/ColorPreviewer.xaml index 6c4500dd..dcad906d 100644 --- a/components/ColorPicker/src/ColorPreviewer.xaml +++ b/components/ColorPicker/src/ColorPreviewer.xaml @@ -17,90 +17,96 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}"> + - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/ColorPicker/src/CommunityToolkit.WinUI.Controls.ColorPicker.csproj b/components/ColorPicker/src/CommunityToolkit.WinUI.Controls.ColorPicker.csproj index 58e04dc2..d0ea2f98 100644 --- a/components/ColorPicker/src/CommunityToolkit.WinUI.Controls.ColorPicker.csproj +++ b/components/ColorPicker/src/CommunityToolkit.WinUI.Controls.ColorPicker.csproj @@ -8,11 +8,12 @@ - - - - - + + + + + + diff --git a/components/ColorPicker/src/Themes/Generic.xaml b/components/ColorPicker/src/Themes/Generic.xaml index c1c5ae68..41854e9f 100644 --- a/components/ColorPicker/src/Themes/Generic.xaml +++ b/components/ColorPicker/src/Themes/Generic.xaml @@ -1,12 +1,12 @@ - - - - + + + From 6006bf3415e15fee232529ebe6f76f25d3c05191 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Sat, 23 Sep 2023 16:15:25 +0200 Subject: [PATCH 05/18] XAML styler Co-Authored-By: robloo <17993847+robloo@users.noreply.github.com> --- .../samples/ColorPickerButtonSample.xaml | 6 ++--- .../samples/ColorPickerSample.xaml | 23 ++++++++++--------- .../ColorPicker/src/ColorPickerButton.xaml | 4 ++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/components/ColorPicker/samples/ColorPickerButtonSample.xaml b/components/ColorPicker/samples/ColorPickerButtonSample.xaml index f2dee171..c473c4b2 100644 --- a/components/ColorPicker/samples/ColorPickerButtonSample.xaml +++ b/components/ColorPicker/samples/ColorPickerButtonSample.xaml @@ -1,4 +1,4 @@ - + + VerticalAlignment="Top" + SelectedColor="LightBlue" /> diff --git a/components/ColorPicker/samples/ColorPickerSample.xaml b/components/ColorPicker/samples/ColorPickerSample.xaml index ee28d607..9ad7a693 100644 --- a/components/ColorPicker/samples/ColorPickerSample.xaml +++ b/components/ColorPicker/samples/ColorPickerSample.xaml @@ -1,4 +1,4 @@ - + - + diff --git a/components/ColorPicker/src/ColorPickerButton.xaml b/components/ColorPicker/src/ColorPickerButton.xaml index 10126cc5..da953e34 100644 --- a/components/ColorPicker/src/ColorPickerButton.xaml +++ b/components/ColorPicker/src/ColorPickerButton.xaml @@ -1,4 +1,4 @@ - + CornerRadius="{ThemeResource ColorPickerButtonPreviewCornerRadius}"> Date: Sat, 23 Sep 2023 16:38:47 +0200 Subject: [PATCH 06/18] Adding icon Co-Authored-By: robloo <17993847+robloo@users.noreply.github.com> --- .../ColorPicker/samples/Assets/ColorPicker.png | Bin 0 -> 5740 bytes components/ColorPicker/samples/Assets/icon.png | Bin 6192 -> 0 bytes .../samples/ColorPicker.Samples.csproj | 9 +++++++++ components/ColorPicker/samples/ColorPicker.md | 2 +- 4 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 components/ColorPicker/samples/Assets/ColorPicker.png delete mode 100644 components/ColorPicker/samples/Assets/icon.png diff --git a/components/ColorPicker/samples/Assets/ColorPicker.png b/components/ColorPicker/samples/Assets/ColorPicker.png new file mode 100644 index 0000000000000000000000000000000000000000..3fbba28ba1c18576659b467afe024ed85848ac58 GIT binary patch literal 5740 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 literal 0 HcmV?d00001 diff --git a/components/ColorPicker/samples/Assets/icon.png b/components/ColorPicker/samples/Assets/icon.png deleted file mode 100644 index 5f574ceca233597ecc5d775fd2f516c0d9685cfa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6192 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 O0000ColorPicker + + + + + + PreserveNewest + + + diff --git a/components/ColorPicker/samples/ColorPicker.md b/components/ColorPicker/samples/ColorPicker.md index f5f82493..94696882 100644 --- a/components/ColorPicker/samples/ColorPicker.md +++ b/components/ColorPicker/samples/ColorPicker.md @@ -9,7 +9,7 @@ category: Controls subcategory: Input discussion-id: 0 issue-id: 0 -icon: assets/icon.png +icon: Assets/ColorPicker.png --- The [ColorPicker](/dotnet/api/microsoft.toolkit.uwp.ui.controls.colorpicker) control lets a user pick a color using a color spectrum, palette, sliders, or text input. From 72e6e9a8e19260331ad449f9f33da5cc7b5e7ea0 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Sat, 23 Sep 2023 16:47:52 +0200 Subject: [PATCH 07/18] Adding missing brushes Co-Authored-By: robloo <17993847+robloo@users.noreply.github.com> --- .../ColorPicker/src/ColorPickerButton.xaml | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/components/ColorPicker/src/ColorPickerButton.xaml b/components/ColorPicker/src/ColorPickerButton.xaml index da953e34..b9ce9048 100644 --- a/components/ColorPicker/src/ColorPickerButton.xaml +++ b/components/ColorPicker/src/ColorPickerButton.xaml @@ -1,16 +1,48 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RGB + HSV + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -214,220 +462,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - RGB - HSV - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -435,8 +469,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - RGB - HSV - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -462,6 +219,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RGB + HSV + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -469,7 +445,8 @@ - - - -