diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml
index c0c83d6a351..1590be25ba6 100644
--- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml
+++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml
@@ -13,8 +13,14 @@
-
-
+
+
+
-
-
-
-
-
+
diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs
new file mode 100644
index 00000000000..130d7e0eddf
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs
@@ -0,0 +1,284 @@
+using Avalonia.Media;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Implements a reduced flat design or flat UI color palette.
+ ///
+ ///
+ /// See:
+ /// - https://htmlcolorcodes.com/color-chart/
+ /// - https://htmlcolorcodes.com/color-chart/flat-design-color-chart/
+ /// - http://designmodo.github.io/Flat-UI/
+ ///
+ /// The GitHub project is licensed as MIT: https://github.com/designmodo/Flat-UI.
+ ///
+ ///
+ public class FlatColorPalette : IColorPalette
+ {
+ // The full Flat UI color chart has 10 rows and 20 columns
+ // See: https://htmlcolorcodes.com/assets/downloads/flat-design-colors/flat-design-color-chart.png
+ // This is a reduced palette for usability
+ private static Color[,] colorChart = new Color[,]
+ {
+ // Pomegranate
+ {
+ Color.FromArgb(0xFF, 0xF9, 0xEB, 0xEA),
+ Color.FromArgb(0xFF, 0xE6, 0xB0, 0xAA),
+ Color.FromArgb(0xFF, 0xCD, 0x61, 0x55),
+ Color.FromArgb(0xFF, 0xA9, 0x32, 0x26),
+ Color.FromArgb(0xFF, 0x7B, 0x24, 0x1C),
+ },
+
+ // Amethyst
+ {
+ Color.FromArgb(0xFF, 0xF5, 0xEE, 0xF8),
+ Color.FromArgb(0xFF, 0xD7, 0xBD, 0xE2),
+ Color.FromArgb(0xFF, 0xAF, 0x7A, 0xC5),
+ Color.FromArgb(0xFF, 0x88, 0x4E, 0xA0),
+ Color.FromArgb(0xFF, 0x63, 0x39, 0x74),
+ },
+
+ // Belize Hole
+ {
+ Color.FromArgb(0xFF, 0xEA, 0xF2, 0xF8),
+ Color.FromArgb(0xFF, 0xA9, 0xCC, 0xE3),
+ Color.FromArgb(0xFF, 0x54, 0x99, 0xC7),
+ Color.FromArgb(0xFF, 0x24, 0x71, 0xA3),
+ Color.FromArgb(0xFF, 0x1A, 0x52, 0x76),
+ },
+
+ // Turquoise
+ {
+ Color.FromArgb(0xFF, 0xE8, 0xF8, 0xF5),
+ Color.FromArgb(0xFF, 0xA3, 0xE4, 0xD7),
+ Color.FromArgb(0xFF, 0x48, 0xC9, 0xB0),
+ Color.FromArgb(0xFF, 0x17, 0xA5, 0x89),
+ Color.FromArgb(0xFF, 0x11, 0x78, 0x64),
+ },
+
+ // Nephritis
+ {
+ Color.FromArgb(0xFF, 0xE9, 0xF7, 0xEF),
+ Color.FromArgb(0xFF, 0xA9, 0xDF, 0xBF),
+ Color.FromArgb(0xFF, 0x52, 0xBE, 0x80),
+ Color.FromArgb(0xFF, 0x22, 0x99, 0x54),
+ Color.FromArgb(0xFF, 0x19, 0x6F, 0x3D),
+ },
+
+ // Sunflower
+ {
+ Color.FromArgb(0xFF, 0xFE, 0xF9, 0xE7),
+ Color.FromArgb(0xFF, 0xF9, 0xE7, 0x9F),
+ Color.FromArgb(0xFF, 0xF4, 0xD0, 0x3F),
+ Color.FromArgb(0xFF, 0xD4, 0xAC, 0x0D),
+ Color.FromArgb(0xFF, 0x9A, 0x7D, 0x0A),
+ },
+
+ // Carrot
+ {
+ Color.FromArgb(0xFF, 0xFD, 0xF2, 0xE9),
+ Color.FromArgb(0xFF, 0xF5, 0xCB, 0xA7),
+ Color.FromArgb(0xFF, 0xEB, 0x98, 0x4E),
+ Color.FromArgb(0xFF, 0xCA, 0x6F, 0x1E),
+ Color.FromArgb(0xFF, 0x93, 0x51, 0x16),
+ },
+
+ // Clouds
+ {
+ Color.FromArgb(0xFF, 0xFD, 0xFE, 0xFE),
+ Color.FromArgb(0xFF, 0xF7, 0xF9, 0xF9),
+ Color.FromArgb(0xFF, 0xF0, 0xF3, 0xF4),
+ Color.FromArgb(0xFF, 0xD0, 0xD3, 0xD4),
+ Color.FromArgb(0xFF, 0x97, 0x9A, 0x9A),
+ },
+
+ // Concrete
+ {
+ Color.FromArgb(0xFF, 0xF4, 0xF6, 0xF6),
+ Color.FromArgb(0xFF, 0xD5, 0xDB, 0xDB),
+ Color.FromArgb(0xFF, 0xAA, 0xB7, 0xB8),
+ Color.FromArgb(0xFF, 0x83, 0x91, 0x92),
+ Color.FromArgb(0xFF, 0x5F, 0x6A, 0x6A),
+ },
+
+ // Wet Asphalt
+ {
+ Color.FromArgb(0xFF, 0xEB, 0xED, 0xEF),
+ Color.FromArgb(0xFF, 0xAE, 0xB6, 0xBF),
+ Color.FromArgb(0xFF, 0x5D, 0x6D, 0x7E),
+ Color.FromArgb(0xFF, 0x2E, 0x40, 0x53),
+ Color.FromArgb(0xFF, 0x21, 0x2F, 0x3C),
+ },
+ };
+
+ ///
+ /// Gets the index of the default shade of colors in this palette.
+ ///
+ public const int DefaultShadeIndex = 2;
+
+ ///
+ /// The index in the color palette of the 'Pomegranate' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int PomegranateIndex = 0;
+
+ ///
+ /// The index in the color palette of the 'Amethyst' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int AmethystIndex = 1;
+
+ ///
+ /// The index in the color palette of the 'BelizeHole' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int BelizeHoleIndex = 2;
+
+ ///
+ /// The index in the color palette of the 'Turquoise' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int TurquoiseIndex = 3;
+
+ ///
+ /// The index in the color palette of the 'Nephritis' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int NephritisIndex = 4;
+
+ ///
+ /// The index in the color palette of the 'Sunflower' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int SunflowerIndex = 5;
+
+ ///
+ /// The index in the color palette of the 'Carrot' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int CarrotIndex = 6;
+
+ ///
+ /// The index in the color palette of the 'Clouds' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int CloudsIndex = 7;
+
+ ///
+ /// The index in the color palette of the 'Concrete' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int ConcreteIndex = 8;
+
+ ///
+ /// The index in the color palette of the 'WetAsphalt' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int WetAsphaltIndex = 9;
+
+ ///
+ public int ColorCount
+ {
+ // Table is transposed compared to the reference chart
+ get => colorChart.GetLength(0);
+ }
+
+ ///
+ public int ShadeCount
+ {
+ // Table is transposed compared to the reference chart
+ get => colorChart.GetLength(1);
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FFC0392B.
+ ///
+ public static Color Pomegranate
+ {
+ get => colorChart[PomegranateIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF9B59B6.
+ ///
+ public static Color Amethyst
+ {
+ get => colorChart[AmethystIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF2980B9.
+ ///
+ public static Color BelizeHole
+ {
+ get => colorChart[BelizeHoleIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF1ABC9C.
+ ///
+ public static Color Turquoise
+ {
+ get => colorChart[TurquoiseIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF27AE60.
+ ///
+ public static Color Nephritis
+ {
+ get => colorChart[NephritisIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FFF1C40F.
+ ///
+ public static Color Sunflower
+ {
+ get => colorChart[SunflowerIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FFE67E22.
+ ///
+ public static Color Carrot
+ {
+ get => colorChart[CarrotIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FFECF0F1.
+ ///
+ public static Color Clouds
+ {
+ get => colorChart[CloudsIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF95A5A6.
+ ///
+ public static Color Concrete
+ {
+ get => colorChart[ConcreteIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF34495E.
+ ///
+ public static Color WetAsphalt
+ {
+ get => colorChart[WetAsphaltIndex, DefaultShadeIndex];
+ }
+
+ ///
+ public Color GetColor(int colorIndex, int shadeIndex)
+ {
+ // Table is transposed compared to the reference chart
+ return colorChart[
+ MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0) - 1),
+ MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1) - 1)];
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs
new file mode 100644
index 00000000000..013e69ce20d
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs
@@ -0,0 +1,136 @@
+using Avalonia.Media;
+using Avalonia.Utilities;
+
+namespace Avalonia.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 */
+ }
+ };
+
+ ///
+ /// 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 => 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 => colorChart.GetLength(1);
+ }
+
+ ///
+ public Color GetColor(int colorIndex, int shadeIndex)
+ {
+ return colorChart[
+ MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0) - 1),
+ MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1) - 1)];
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalettes/IColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/IColorPalette.cs
new file mode 100644
index 00000000000..7c6ebc3f6a3
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/IColorPalette.cs
@@ -0,0 +1,38 @@
+using Avalonia.Media;
+
+namespace Avalonia.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.
+ ///
+ ///
+ /// Represents total columns in a table.
+ ///
+ 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.
+ ///
+ ///
+ /// Represents total rows in a table.
+ ///
+ 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/src/Avalonia.Controls.ColorPicker/ColorPalettes/SixteenColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/SixteenColorPalette.cs
new file mode 100644
index 00000000000..f3abfdfd7fb
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/SixteenColorPalette.cs
@@ -0,0 +1,302 @@
+using Avalonia.Media;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Implements the standard sixteen color palette from the HTML 4.01 specification.
+ ///
+ ///
+ /// See https://en.wikipedia.org/wiki/Web_colors#HTML_color_names.
+ ///
+ public class SixteenColorPalette : IColorPalette
+ {
+ // The 16 standard colors from HTML and early Windows computers
+ // https://en.wikipedia.org/wiki/List_of_software_palettes
+ // https://en.wikipedia.org/wiki/Web_colors#HTML_color_names
+ private static Color[,] colorChart = new Color[,]
+ {
+ {
+ Colors.White,
+ Colors.Silver
+ },
+ {
+ Colors.Gray,
+ Colors.Black
+ },
+ {
+ Colors.Red,
+ Colors.Maroon
+ },
+ {
+ Colors.Yellow,
+ Colors.Olive
+ },
+ {
+ Colors.Lime,
+ Colors.Green
+ },
+ {
+ Colors.Aqua,
+ Colors.Teal
+ },
+ {
+ Colors.Blue,
+ Colors.Navy
+ },
+ {
+ Colors.Fuchsia,
+ Colors.Purple
+ }
+ };
+
+ ///
+ /// Gets the index of the default shade of colors in this palette.
+ ///
+ public const int DefaultShadeIndex = 0;
+
+ ///
+ /// The index in the color palette of the 'White' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int WhiteIndex = 0;
+
+ ///
+ /// The index in the color palette of the 'Silver' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int SilverIndex = 1;
+
+ ///
+ /// The index in the color palette of the 'Gray' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int GrayIndex = 2;
+
+ ///
+ /// The index in the color palette of the 'Black' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int BlackIndex = 3;
+
+ ///
+ /// The index in the color palette of the 'Red' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int RedIndex = 4;
+
+ ///
+ /// The index in the color palette of the 'Maroon' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int MaroonIndex = 5;
+
+ ///
+ /// The index in the color palette of the 'Yellow' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int YellowIndex = 6;
+
+ ///
+ /// The index in the color palette of the 'Olive' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int OliveIndex = 7;
+
+ ///
+ /// The index in the color palette of the 'Lime' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int LimeIndex = 8;
+
+ ///
+ /// The index in the color palette of the 'Green' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int GreenIndex = 9;
+
+ ///
+ /// The index in the color palette of the 'Aqua' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int AquaIndex = 10;
+
+ ///
+ /// The index in the color palette of the 'Teal' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int TealIndex = 11;
+
+ ///
+ /// The index in the color palette of the 'Blue' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int BlueIndex = 12;
+
+ ///
+ /// The index in the color palette of the 'Navy' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int NavyIndex = 13;
+
+ ///
+ /// The index in the color palette of the 'Fuchsia' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int FuchsiaIndex = 14;
+
+ ///
+ /// The index in the color palette of the 'Purple' color.
+ /// This index can correspond to multiple color shades.
+ ///
+ public const int PurpleIndex = 15;
+
+ ///
+ public int ColorCount
+ {
+ get => colorChart.GetLength(0);
+ }
+
+ ///
+ public int ShadeCount
+ {
+ get => colorChart.GetLength(1);
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FFFFFFFF.
+ ///
+ public static Color White
+ {
+ get => colorChart[WhiteIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FFC0C0C0.
+ ///
+ public static Color Silver
+ {
+ get => colorChart[SilverIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF808080.
+ ///
+ public static Color Gray
+ {
+ get => colorChart[GrayIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF000000.
+ ///
+ public static Color Black
+ {
+ get => colorChart[BlackIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FFFF0000.
+ ///
+ public static Color Red
+ {
+ get => colorChart[RedIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF800000.
+ ///
+ public static Color Maroon
+ {
+ get => colorChart[MaroonIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FFFFFF00.
+ ///
+ public static Color Yellow
+ {
+ get => colorChart[YellowIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF808000.
+ ///
+ public static Color Olive
+ {
+ get => colorChart[OliveIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF00FF00.
+ ///
+ public static Color Lime
+ {
+ get => colorChart[LimeIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF008000.
+ ///
+ public static Color Green
+ {
+ get => colorChart[GreenIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF00FFFF.
+ ///
+ public static Color Aqua
+ {
+ get => colorChart[AquaIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF008080.
+ ///
+ public static Color Teal
+ {
+ get => colorChart[TealIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF0000FF.
+ ///
+ public static Color Blue
+ {
+ get => colorChart[BlueIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF000080.
+ ///
+ public static Color Navy
+ {
+ get => colorChart[NavyIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FFFF00FF.
+ ///
+ public static Color Fuchsia
+ {
+ get => colorChart[FuchsiaIndex, DefaultShadeIndex];
+ }
+
+ ///
+ /// Gets the palette defined color that has an ARGB value of #FF800080.
+ ///
+ public static Color Purple
+ {
+ get => colorChart[PurpleIndex, DefaultShadeIndex];
+ }
+
+ ///
+ public Color GetColor(int colorIndex, int shadeIndex)
+ {
+ return colorChart[
+ MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0) - 1),
+ MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1) - 1)];
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs
new file mode 100644
index 00000000000..39369bcbdbf
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs
@@ -0,0 +1,19 @@
+namespace Avalonia.Controls
+{
+ ///
+ /// Presents a color for user editing using a spectrum, palette and component sliders within a drop down.
+ /// Editing is available when the drop down flyout is opened; otherwise, only the preview color is shown.
+ ///
+ public class ColorPicker : ColorView
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ColorPicker() : base()
+ {
+ // Completely ignore property changes here
+ // The ColorView in the control template is responsible to manage this
+ base.ignorePropertyChanged = true;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs
index 0fa6ab80835..e1ffbb7eae0 100644
--- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs
+++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs
@@ -16,11 +16,11 @@ public partial class ColorPreviewer
defaultBindingMode: BindingMode.TwoWay);
///
- /// Defines the property.
+ /// Defines the property.
///
- public static readonly StyledProperty ShowAccentColorsProperty =
+ public static readonly StyledProperty IsAccentColorsVisibleProperty =
AvaloniaProperty.Register(
- nameof(ShowAccentColors),
+ nameof(IsAccentColorsVisible),
true);
///
@@ -38,13 +38,13 @@ public HsvColor HsvColor
}
///
- /// Gets or sets a value indicating whether accent colors are shown along
+ /// Gets or sets a value indicating whether accent colors are visible along
/// with the preview color.
///
- public bool ShowAccentColors
+ public bool IsAccentColorsVisible
{
- get => GetValue(ShowAccentColorsProperty);
- set => SetValue(ShowAccentColorsProperty, value);
+ get => GetValue(IsAccentColorsVisibleProperty);
+ set => SetValue(IsAccentColorsVisibleProperty, value);
}
}
}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs
index d04ddf4bd6b..6f494305055 100644
--- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs
+++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs
@@ -10,10 +10,10 @@ namespace Avalonia.Controls.Primitives
///
/// Presents a preview color with optional accent colors.
///
- [TemplatePart(Name = nameof(AccentDec1Border), Type = typeof(Border))]
- [TemplatePart(Name = nameof(AccentDec2Border), Type = typeof(Border))]
- [TemplatePart(Name = nameof(AccentInc1Border), Type = typeof(Border))]
- [TemplatePart(Name = nameof(AccentInc2Border), Type = typeof(Border))]
+ [TemplatePart("PART_AccentDecrement1Border", typeof(Border))]
+ [TemplatePart("PART_AccentDecrement2Border", typeof(Border))]
+ [TemplatePart("PART_AccentIncrement1Border", typeof(Border))]
+ [TemplatePart("PART_AccentIncrement2Border", typeof(Border))]
public partial class ColorPreviewer : TemplatedControl
{
///
@@ -24,10 +24,11 @@ public partial class ColorPreviewer : TemplatedControl
private bool eventsConnected = false;
- private Border? AccentDec1Border;
- private Border? AccentDec2Border;
- private Border? AccentInc1Border;
- private Border? AccentInc2Border;
+ // XAML template parts
+ private Border? _accentDecrement1Border;
+ private Border? _accentDecrement2Border;
+ private Border? _accentIncrement1Border;
+ private Border? _accentIncrement2Border;
///
/// Initializes a new instance of the class.
@@ -45,20 +46,20 @@ private void ConnectEvents(bool connected)
if (connected == true && eventsConnected == false)
{
// Add all events
- if (AccentDec1Border != null) { AccentDec1Border.PointerPressed += AccentBorder_PointerPressed; }
- if (AccentDec2Border != null) { AccentDec2Border.PointerPressed += AccentBorder_PointerPressed; }
- if (AccentInc1Border != null) { AccentInc1Border.PointerPressed += AccentBorder_PointerPressed; }
- if (AccentInc2Border != null) { AccentInc2Border.PointerPressed += AccentBorder_PointerPressed; }
+ if (_accentDecrement1Border != null) { _accentDecrement1Border.PointerPressed += AccentBorder_PointerPressed; }
+ if (_accentDecrement2Border != null) { _accentDecrement2Border.PointerPressed += AccentBorder_PointerPressed; }
+ if (_accentIncrement1Border != null) { _accentIncrement1Border.PointerPressed += AccentBorder_PointerPressed; }
+ if (_accentIncrement2Border != null) { _accentIncrement2Border.PointerPressed += AccentBorder_PointerPressed; }
eventsConnected = true;
}
else if (connected == false && eventsConnected == true)
{
// Remove all events
- if (AccentDec1Border != null) { AccentDec1Border.PointerPressed -= AccentBorder_PointerPressed; }
- if (AccentDec2Border != null) { AccentDec2Border.PointerPressed -= AccentBorder_PointerPressed; }
- if (AccentInc1Border != null) { AccentInc1Border.PointerPressed -= AccentBorder_PointerPressed; }
- if (AccentInc2Border != null) { AccentInc2Border.PointerPressed -= AccentBorder_PointerPressed; }
+ if (_accentDecrement1Border != null) { _accentDecrement1Border.PointerPressed -= AccentBorder_PointerPressed; }
+ if (_accentDecrement2Border != null) { _accentDecrement2Border.PointerPressed -= AccentBorder_PointerPressed; }
+ if (_accentIncrement1Border != null) { _accentIncrement1Border.PointerPressed -= AccentBorder_PointerPressed; }
+ if (_accentIncrement2Border != null) { _accentIncrement2Border.PointerPressed -= AccentBorder_PointerPressed; }
eventsConnected = false;
}
@@ -70,10 +71,10 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
// Remove any existing events present if the control was previously loaded then unloaded
ConnectEvents(false);
- AccentDec1Border = e.NameScope.Find(nameof(AccentDec1Border));
- AccentDec2Border = e.NameScope.Find(nameof(AccentDec2Border));
- AccentInc1Border = e.NameScope.Find(nameof(AccentInc1Border));
- AccentInc2Border = e.NameScope.Find(nameof(AccentInc2Border));
+ _accentDecrement1Border = e.NameScope.Find("PART_AccentDecrement1Border");
+ _accentDecrement2Border = e.NameScope.Find("PART_AccentDecrement2Border");
+ _accentIncrement1Border = e.NameScope.Find("PART_AccentIncrement1Border");
+ _accentIncrement2Border = e.NameScope.Find("PART_AccentIncrement2Border");
// Must connect after controls are found
ConnectEvents(true);
@@ -116,15 +117,15 @@ private void AccentBorder_PointerPressed(object? sender, PointerPressedEventArgs
// Get the value component delta
try
{
- accentStep = int.Parse(border?.Tag?.ToString() ?? "", CultureInfo.InvariantCulture);
+ accentStep = int.Parse(border?.Tag?.ToString() ?? "0", CultureInfo.InvariantCulture);
}
catch { }
- HsvColor newHsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep);
- HsvColor oldHsvColor = HsvColor;
-
- HsvColor = newHsvColor;
- OnColorChanged(new ColorChangedEventArgs(oldHsvColor.ToRgb(), newHsvColor.ToRgb()));
+ if (accentStep != 0)
+ {
+ // ColorChanged will be invoked in OnPropertyChanged if the value is different
+ HsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep);
+ }
}
}
}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs
index 31bd296288f..e2a34a7f909 100644
--- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs
+++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs
@@ -49,12 +49,12 @@ public partial class ColorSlider
true);
///
- /// Defines the property.
+ /// Defines the property.
///
- public static readonly StyledProperty IsAutoUpdatingEnabledProperty =
+ public static readonly StyledProperty IsRoundingEnabledProperty =
AvaloniaProperty.Register(
- nameof(IsAutoUpdatingEnabled),
- true);
+ nameof(IsRoundingEnabled),
+ false);
///
/// Defines the property.
@@ -120,16 +120,16 @@ public bool IsAlphaMaxForced
}
///
- /// Gets or sets a value indicating whether automatic background and foreground updates will be
- /// calculated when the set color changes.
+ /// Gets or sets a value indicating whether rounding of color component values is enabled.
///
///
- /// This can be disabled for performance reasons when working with multiple sliders.
+ /// This is applicable for the HSV color model only. The struct uses double
+ /// values while the struct uses byte. Only double types need rounding.
///
- public bool IsAutoUpdatingEnabled
+ public bool IsRoundingEnabled
{
- get => GetValue(IsAutoUpdatingEnabledProperty);
- set => SetValue(IsAutoUpdatingEnabledProperty, value);
+ get => GetValue(IsRoundingEnabledProperty);
+ set => SetValue(IsRoundingEnabledProperty, value);
}
///
diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
index 3c38c6ed1b6..b662d202237 100644
--- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
+++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
@@ -20,8 +20,16 @@ public partial class ColorSlider : Slider
///
public event EventHandler? ColorChanged;
- private const double MaxHue = 359.99999999999999999; // 17 decimal places
- private bool disableUpdates = false;
+ ///
+ /// Defines the maximum hue component value
+ /// (other components are always 0..100 or 0.255).
+ ///
+ ///
+ /// This should match the default property.
+ ///
+ private const double MaxHue = 359;
+
+ protected bool ignorePropertyChanged = false;
///
/// Initializes a new instance of the class.
@@ -107,21 +115,41 @@ private async void UpdateBackground()
}
}
+ ///
+ /// Rounds the component values of the given .
+ /// This is useful for user-display and to ensure a color matches user selection exactly.
+ ///
+ /// The to round component values for.
+ /// A new with rounded component values.
+ private HsvColor RoundComponentValues(HsvColor hsvColor)
+ {
+ return new HsvColor(
+ Math.Round(hsvColor.A, 2, MidpointRounding.AwayFromZero),
+ Math.Round(hsvColor.H, 0, MidpointRounding.AwayFromZero),
+ Math.Round(hsvColor.S, 2, MidpointRounding.AwayFromZero),
+ Math.Round(hsvColor.V, 2, MidpointRounding.AwayFromZero));
+ }
+
///
/// Updates the slider property values by applying the current color.
///
///
/// Warning: This will trigger property changed updates.
- /// Consider using externally.
+ /// Consider using externally.
///
private void SetColorToSliderValues()
{
- var hsvColor = HsvColor;
- var rgbColor = Color;
var component = ColorComponent;
if (ColorModel == ColorModel.Hsva)
{
+ var hsvColor = HsvColor;
+
+ if (IsRoundingEnabled)
+ {
+ hsvColor = RoundComponentValues(hsvColor);
+ }
+
// Note: Components converted into a usable range for the user
switch (component)
{
@@ -149,6 +177,8 @@ private void SetColorToSliderValues()
}
else
{
+ var rgbColor = Color;
+
switch (component)
{
case ColorComponent.Alpha:
@@ -183,13 +213,12 @@ private void SetColorToSliderValues()
HsvColor hsvColor = new HsvColor();
Color rgbColor = new Color();
double sliderPercent = Value / (Maximum - Minimum);
-
- var baseHsvColor = HsvColor;
- var baseRgbColor = Color;
var component = ColorComponent;
if (ColorModel == ColorModel.Hsva)
{
+ var baseHsvColor = HsvColor;
+
switch (component)
{
case ColorComponent.Alpha:
@@ -214,10 +243,12 @@ private void SetColorToSliderValues()
}
}
- return (hsvColor.ToRgb(), hsvColor);
+ rgbColor = hsvColor.ToRgb();
}
else
{
+ var baseRgbColor = Color;
+
byte componentValue = Convert.ToByte(MathUtilities.Clamp(sliderPercent * 255, 0, 255));
switch (component)
@@ -236,8 +267,15 @@ private void SetColorToSliderValues()
break;
}
- return (rgbColor, rgbColor.ToHsv());
+ hsvColor = rgbColor.ToHsv();
}
+
+ if (IsRoundingEnabled)
+ {
+ hsvColor = RoundComponentValues(hsvColor);
+ }
+
+ return (rgbColor, hsvColor);
}
///
@@ -306,7 +344,7 @@ private Color GetEquivalentBackgroundColor(Color rgbColor)
///
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
- if (disableUpdates)
+ if (ignorePropertyChanged)
{
base.OnPropertyChanged(change);
return;
@@ -315,54 +353,59 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
// Always keep the two color properties in sync
if (change.Property == ColorProperty)
{
- disableUpdates = true;
+ ignorePropertyChanged = true;
HsvColor = Color.ToHsv();
- if (IsAutoUpdatingEnabled)
- {
- SetColorToSliderValues();
- UpdateBackground();
- }
-
+ SetColorToSliderValues();
+ UpdateBackground();
UpdatePseudoClasses();
+
OnColorChanged(new ColorChangedEventArgs(
change.GetOldValue(),
change.GetNewValue()));
- disableUpdates = false;
+ ignorePropertyChanged = false;
+ }
+ else if (change.Property == ColorModelProperty)
+ {
+ ignorePropertyChanged = true;
+
+ SetColorToSliderValues();
+ UpdateBackground();
+ UpdatePseudoClasses();
+
+ ignorePropertyChanged = false;
}
else if (change.Property == HsvColorProperty)
{
- disableUpdates = true;
+ ignorePropertyChanged = true;
Color = HsvColor.ToRgb();
- if (IsAutoUpdatingEnabled)
- {
- SetColorToSliderValues();
- UpdateBackground();
- }
-
+ SetColorToSliderValues();
+ UpdateBackground();
UpdatePseudoClasses();
+
OnColorChanged(new ColorChangedEventArgs(
change.GetOldValue().ToRgb(),
change.GetNewValue().ToRgb()));
- disableUpdates = false;
+ ignorePropertyChanged = false;
+ }
+ else if (change.Property == IsRoundingEnabledProperty)
+ {
+ SetColorToSliderValues();
}
else if (change.Property == BoundsProperty)
{
- if (IsAutoUpdatingEnabled)
- {
- UpdateBackground();
- }
+ UpdateBackground();
}
else if (change.Property == ValueProperty ||
change.Property == MinimumProperty ||
change.Property == MaximumProperty)
{
- disableUpdates = true;
+ ignorePropertyChanged = true;
Color oldColor = Color;
(var color, var hsvColor) = GetColorFromSliderValues();
@@ -381,7 +424,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
UpdatePseudoClasses();
OnColorChanged(new ColorChangedEventArgs(oldColor, Color));
- disableUpdates = false;
+ ignorePropertyChanged = false;
}
base.OnPropertyChanged(change);
diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
index 587a89ee38a..00d84f5dd3e 100644
--- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
+++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
@@ -97,7 +97,7 @@ public partial class ColorSpectrum
/// Gets or sets the currently selected color in the RGB color model.
///
///
- /// For control authors use instead to avoid loss
+ /// For control authors, use instead to avoid loss
/// of precision and color drifting.
///
public Color Color
diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
index 7e6b70a1465..bd44161a42a 100644
--- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
+++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
@@ -44,7 +44,6 @@ public partial class ColorSpectrum : TemplatedControl
private bool _updatingColor = false;
private bool _updatingHsvColor = false;
- private bool _isPointerOver = false;
private bool _isPointerPressed = false;
private bool _shouldShowLargeSelection = false;
private List _hsvValues = new List();
@@ -851,7 +850,6 @@ private void UpdateEllipse()
///
private void InputTarget_PointerEntered(object? sender, PointerEventArgs args)
{
- _isPointerOver = true;
UpdatePseudoClasses();
args.Handled = true;
}
@@ -859,7 +857,6 @@ private void InputTarget_PointerEntered(object? sender, PointerEventArgs args)
///
private void InputTarget_PointerExited(object? sender, PointerEventArgs args)
{
- _isPointerOver = false;
UpdatePseudoClasses();
args.Handled = true;
}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs
new file mode 100644
index 00000000000..b76059037b5
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs
@@ -0,0 +1,495 @@
+using System.Collections.Generic;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Media;
+
+namespace Avalonia.Controls
+{
+ ///
+ public partial class ColorView
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ColorProperty =
+ AvaloniaProperty.Register(
+ nameof(Color),
+ Colors.White,
+ defaultBindingMode: BindingMode.TwoWay,
+ coerce: CoerceColor) ;
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ColorModelProperty =
+ AvaloniaProperty.Register(
+ nameof(ColorModel),
+ ColorModel.Rgba);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ColorSpectrumComponentsProperty =
+ AvaloniaProperty.Register(
+ nameof(ColorSpectrumComponents),
+ ColorSpectrumComponents.HueSaturation);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ColorSpectrumShapeProperty =
+ AvaloniaProperty.Register(
+ nameof(ColorSpectrumShape),
+ ColorSpectrumShape.Box);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty HsvColorProperty =
+ AvaloniaProperty.Register(
+ nameof(HsvColor),
+ Colors.White.ToHsv(),
+ defaultBindingMode: BindingMode.TwoWay,
+ coerce: CoerceHsvColor);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsAccentColorsVisibleProperty =
+ AvaloniaProperty.Register(
+ nameof(IsAccentColorsVisible),
+ true);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsAlphaEnabledProperty =
+ AvaloniaProperty.Register(
+ nameof(IsAlphaEnabled),
+ true);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsAlphaVisibleProperty =
+ AvaloniaProperty.Register(
+ nameof(IsAlphaVisible),
+ true);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsColorComponentsVisibleProperty =
+ AvaloniaProperty.Register(
+ nameof(IsColorComponentsVisible),
+ true);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsColorModelVisibleProperty =
+ AvaloniaProperty.Register(
+ nameof(IsColorModelVisible),
+ true);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsColorPaletteVisibleProperty =
+ AvaloniaProperty.Register(
+ nameof(IsColorPaletteVisible),
+ true);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsColorPreviewVisibleProperty =
+ AvaloniaProperty.Register(
+ nameof(IsColorPreviewVisible),
+ true);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsColorSpectrumVisibleProperty =
+ AvaloniaProperty.Register(
+ nameof(IsColorSpectrumVisible),
+ true);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsColorSpectrumSliderVisibleProperty =
+ AvaloniaProperty.Register(
+ nameof(IsColorSpectrumSliderVisible),
+ true);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsComponentSliderVisibleProperty =
+ AvaloniaProperty.Register(
+ nameof(IsComponentSliderVisible),
+ true);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsComponentTextInputVisibleProperty =
+ AvaloniaProperty.Register(
+ nameof(IsComponentTextInputVisible),
+ true);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsHexInputVisibleProperty =
+ AvaloniaProperty.Register(
+ nameof(IsHexInputVisible),
+ true);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MaxHueProperty =
+ AvaloniaProperty.Register(
+ nameof(MaxHue),
+ 359);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MaxSaturationProperty =
+ AvaloniaProperty.Register(
+ nameof(MaxSaturation),
+ 100);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MaxValueProperty =
+ AvaloniaProperty.Register(
+ nameof(MaxValue),
+ 100);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinHueProperty =
+ AvaloniaProperty.Register(
+ nameof(MinHue),
+ 0);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinSaturationProperty =
+ AvaloniaProperty.Register(
+ nameof(MinSaturation),
+ 0);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinValueProperty =
+ AvaloniaProperty.Register(
+ nameof(MinValue),
+ 0);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty?> PaletteColorsProperty =
+ AvaloniaProperty.Register?>(
+ nameof(PaletteColors),
+ null);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PaletteColumnCountProperty =
+ AvaloniaProperty.Register(
+ nameof(PaletteColumnCount),
+ 4);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PaletteProperty =
+ AvaloniaProperty.Register(
+ nameof(Palette),
+ null);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty SelectedIndexProperty =
+ AvaloniaProperty.Register(
+ nameof(SelectedIndex),
+ (int)ColorViewTab.Spectrum);
+
+ ///
+ public Color Color
+ {
+ get => GetValue(ColorProperty);
+ set => SetValue(ColorProperty, value);
+ }
+
+ ///
+ ///
+ /// This property is only applicable to the components tab.
+ /// The spectrum tab must always be in HSV and the palette tab contains only pre-defined colors.
+ ///
+ public ColorModel ColorModel
+ {
+ get => GetValue(ColorModelProperty);
+ set => SetValue(ColorModelProperty, value);
+ }
+
+ ///
+ public ColorSpectrumComponents ColorSpectrumComponents
+ {
+ get => GetValue(ColorSpectrumComponentsProperty);
+ set => SetValue(ColorSpectrumComponentsProperty, value);
+ }
+
+ ///
+ public ColorSpectrumShape ColorSpectrumShape
+ {
+ get => GetValue(ColorSpectrumShapeProperty);
+ set => SetValue(ColorSpectrumShapeProperty, value);
+ }
+
+ ///
+ public HsvColor HsvColor
+ {
+ get => GetValue(HsvColorProperty);
+ set => SetValue(HsvColorProperty, value);
+ }
+
+ ///
+ public bool IsAccentColorsVisible
+ {
+ get => GetValue(IsAccentColorsVisibleProperty);
+ set => SetValue(IsAccentColorsVisibleProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the alpha component is enabled.
+ /// When disabled (set to false) the alpha component will be fixed to maximum and
+ /// editing controls disabled.
+ ///
+ public bool IsAlphaEnabled
+ {
+ get => GetValue(IsAlphaEnabledProperty);
+ set => SetValue(IsAlphaEnabledProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the alpha component editing controls
+ /// (Slider(s) and TextBox) are visible. When hidden, the existing alpha component
+ /// value is maintained.
+ ///
+ ///
+ /// Note that also controls the alpha
+ /// component TextBox visibility.
+ ///
+ public bool IsAlphaVisible
+ {
+ get => GetValue(IsAlphaVisibleProperty);
+ set => SetValue(IsAlphaVisibleProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the color components tab/panel/page (subview) is visible.
+ ///
+ public bool IsColorComponentsVisible
+ {
+ get => GetValue(IsColorComponentsVisibleProperty);
+ set => SetValue(IsColorComponentsVisibleProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the active color model indicator/selector is visible.
+ ///
+ public bool IsColorModelVisible
+ {
+ get => GetValue(IsColorModelVisibleProperty);
+ set => SetValue(IsColorModelVisibleProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the color palette tab/panel/page (subview) is visible.
+ ///
+ public bool IsColorPaletteVisible
+ {
+ get => GetValue(IsColorPaletteVisibleProperty);
+ set => SetValue(IsColorPaletteVisibleProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the color preview is visible.
+ ///
+ ///
+ /// Note that accent color visibility is controlled separately by
+ /// .
+ ///
+ public bool IsColorPreviewVisible
+ {
+ get => GetValue(IsColorPreviewVisibleProperty);
+ set => SetValue(IsColorPreviewVisibleProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the color spectrum tab/panel/page (subview) is visible.
+ ///
+ public bool IsColorSpectrumVisible
+ {
+ get => GetValue(IsColorSpectrumVisibleProperty);
+ set => SetValue(IsColorSpectrumVisibleProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the color spectrum's third component slider
+ /// is visible.
+ ///
+ public bool IsColorSpectrumSliderVisible
+ {
+ get => GetValue(IsColorSpectrumSliderVisibleProperty);
+ set => SetValue(IsColorSpectrumSliderVisibleProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether color component sliders are visible.
+ ///
+ ///
+ /// All color components are controlled by this property but alpha can also be
+ /// controlled with .
+ ///
+ public bool IsComponentSliderVisible
+ {
+ get => GetValue(IsComponentSliderVisibleProperty);
+ set => SetValue(IsComponentSliderVisibleProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether color component text inputs are visible.
+ ///
+ ///
+ /// All color components are controlled by this property but alpha can also be
+ /// controlled with .
+ ///
+ public bool IsComponentTextInputVisible
+ {
+ get => GetValue(IsComponentTextInputVisibleProperty);
+ set => SetValue(IsComponentTextInputVisibleProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the hexadecimal color value text input
+ /// is visible.
+ ///
+ public bool IsHexInputVisible
+ {
+ get => GetValue(IsHexInputVisibleProperty);
+ set => SetValue(IsHexInputVisibleProperty, value);
+ }
+
+ ///
+ public int MaxHue
+ {
+ get => GetValue(MaxHueProperty);
+ set => SetValue(MaxHueProperty, value);
+ }
+
+ ///
+ public int MaxSaturation
+ {
+ get => GetValue(MaxSaturationProperty);
+ set => SetValue(MaxSaturationProperty, value);
+ }
+
+ ///
+ public int MaxValue
+ {
+ get => GetValue(MaxValueProperty);
+ set => SetValue(MaxValueProperty, value);
+ }
+
+ ///
+ public int MinHue
+ {
+ get => GetValue(MinHueProperty);
+ set => SetValue(MinHueProperty, value);
+ }
+
+ ///
+ public int MinSaturation
+ {
+ get => GetValue(MinSaturationProperty);
+ set => SetValue(MinSaturationProperty, value);
+ }
+
+ ///
+ public int MinValue
+ {
+ get => GetValue(MinValueProperty);
+ set => SetValue(MinValueProperty, value);
+ }
+
+ ///
+ /// Gets or sets the collection of individual colors in the palette.
+ ///
+ ///
+ /// This is not commonly set manually. Instead, it should be set automatically by
+ /// providing an to the property.
+ ///
+ /// Also note that this property is what should be bound in the control template.
+ /// is too high-level to use on its own.
+ ///
+ public IEnumerable? PaletteColors
+ {
+ get => GetValue(PaletteColorsProperty);
+ set => SetValue(PaletteColorsProperty, value);
+ }
+
+ ///
+ /// Gets or sets the number of colors in each row (section) of the color palette.
+ /// Within a standard palette, rows are shades and columns are colors.
+ ///
+ ///
+ /// This is not commonly set manually. Instead, it should be set automatically by
+ /// providing an to the property.
+ ///
+ /// Also note that this property is what should be bound in the control template.
+ /// is too high-level to use on its own.
+ ///
+ public int PaletteColumnCount
+ {
+ get => GetValue(PaletteColumnCountProperty);
+ set => SetValue(PaletteColumnCountProperty, value);
+ }
+
+ ///
+ /// Gets or sets the color palette.
+ ///
+ ///
+ /// This will automatically set both and
+ /// overwriting any existing values.
+ ///
+ public IColorPalette? Palette
+ {
+ get => GetValue(PaletteProperty);
+ set => SetValue(PaletteProperty, value);
+ }
+
+ ///
+ /// Gets or sets the index of the selected tab/panel/page (subview).
+ ///
+ public int SelectedIndex
+ {
+ get => GetValue(SelectedIndexProperty);
+ set => SetValue(SelectedIndexProperty, value);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs
new file mode 100644
index 00000000000..89f1afb1acd
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs
@@ -0,0 +1,379 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using Avalonia.Controls.Converters;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
+using Avalonia.Media;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Presents a color for user editing using a spectrum, palette and component sliders.
+ ///
+ [TemplatePart("PART_HexTextBox", typeof(TextBox))]
+ [TemplatePart("PART_TabControl", typeof(TabControl))]
+ public partial class ColorView : TemplatedControl
+ {
+ ///
+ /// Event for when the selected color changes within the slider.
+ ///
+ public event EventHandler? ColorChanged;
+
+ // XAML template parts
+ private TextBox? _hexTextBox;
+ private TabControl? _tabControl;
+
+ private ColorToHexConverter colorToHexConverter = new ColorToHexConverter();
+ protected bool ignorePropertyChanged = false;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ColorView() : base()
+ {
+ }
+
+ ///
+ /// Gets the value of the hex TextBox and sets it as the current .
+ /// If invalid, the TextBox hex text will revert back to the last valid color.
+ ///
+ private void GetColorFromHexTextBox()
+ {
+ if (_hexTextBox != null)
+ {
+ var convertedColor = colorToHexConverter.ConvertBack(_hexTextBox.Text, typeof(Color), null, CultureInfo.CurrentCulture);
+
+ if (convertedColor is Color color)
+ {
+ Color = color;
+ }
+
+ // Re-apply the hex value
+ // This ensure the hex color value is always valid and formatted correctly
+ SetColorToHexTextBox();
+ }
+ }
+
+ ///
+ /// Sets the current to the hex TextBox.
+ ///
+ private void SetColorToHexTextBox()
+ {
+ if (_hexTextBox != null)
+ {
+ _hexTextBox.Text = colorToHexConverter.Convert(Color, typeof(string), null, CultureInfo.CurrentCulture) as string;
+ }
+ }
+
+ ///
+ /// Validates the tab/panel/page selection taking into account the visibility of each item
+ /// as well as the current selection.
+ ///
+ ///
+ /// Derived controls may re-implement this based on their default style / control template
+ /// and any specialized selection needs.
+ ///
+ protected virtual void ValidateSelection()
+ {
+ if (_tabControl != null &&
+ _tabControl.Items != null)
+ {
+ // Determine the number of visible tab items
+ int numVisibleItems = 0;
+ foreach (var item in _tabControl.Items)
+ {
+ if (item is Control control &&
+ control.IsVisible)
+ {
+ numVisibleItems++;
+ }
+ }
+
+ // Verify the selection
+ if (numVisibleItems > 0)
+ {
+ object? selectedItem = null;
+
+ if (_tabControl.SelectedItem == null &&
+ _tabControl.ItemCount > 0)
+ {
+ // As a failsafe, forcefully select the first item
+ foreach (var item in _tabControl.Items)
+ {
+ selectedItem = item;
+ break;
+ }
+ }
+ else
+ {
+ selectedItem = _tabControl.SelectedItem;
+ }
+
+ if (selectedItem is Control selectedControl &&
+ selectedControl.IsVisible == false)
+ {
+ // Select the first visible item instead
+ foreach (var item in _tabControl.Items)
+ {
+ if (item is Control control &&
+ control.IsVisible)
+ {
+ selectedItem = item;
+ break;
+ }
+ }
+ }
+
+ _tabControl.SelectedItem = selectedItem;
+ _tabControl.IsVisible = true;
+ }
+ else
+ {
+ // Special case when all items are hidden
+ // If TabControl ever properly supports no selected item /
+ // all items hidden this can be removed
+ _tabControl.SelectedItem = null;
+ _tabControl.IsVisible = false;
+ }
+
+ // Hide the "tab strip" if there is only one tab
+ // This allows, for example, to view only the palette
+ /*
+ var itemsPresenter = _tabControl.FindDescendantOfType();
+ if (itemsPresenter != null)
+ {
+ if (numVisibleItems == 1)
+ {
+ itemsPresenter.IsVisible = false;
+ }
+ else
+ {
+ itemsPresenter.IsVisible = true;
+ }
+ }
+ */
+
+ // Note that if externally the SelectedIndex is set to 4 or something
+ // outside the valid range, the TabControl will ignore it and replace it
+ // with a valid SelectedIndex. This however is not propagated back through
+ // the TwoWay binding in the control template so the SelectedIndex and
+ // SelectedIndex become out of sync.
+ //
+ // The work-around for this is done here where SelectedIndex is forcefully
+ // synchronized with whatever the TabControl property value is. This is
+ // possible since selection validation is already done by this method.
+ SelectedIndex = _tabControl.SelectedIndex;
+ }
+
+ return;
+ }
+
+ ///
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ if (_hexTextBox != null)
+ {
+ _hexTextBox.KeyDown -= HexTextBox_KeyDown;
+ _hexTextBox.LostFocus -= HexTextBox_LostFocus;
+ }
+
+ _hexTextBox = e.NameScope.Find("PART_HexTextBox");
+ _tabControl = e.NameScope.Find("PART_TabControl");
+
+ SetColorToHexTextBox();
+
+ if (_hexTextBox != null)
+ {
+ _hexTextBox.KeyDown += HexTextBox_KeyDown;
+ _hexTextBox.LostFocus += HexTextBox_LostFocus;
+ }
+
+ base.OnApplyTemplate(e);
+ ValidateSelection();
+ }
+
+ ///
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ if (ignorePropertyChanged)
+ {
+ base.OnPropertyChanged(change);
+ return;
+ }
+
+ // Always keep the two color properties in sync
+ if (change.Property == ColorProperty)
+ {
+ ignorePropertyChanged = true;
+
+ HsvColor = Color.ToHsv();
+ SetColorToHexTextBox();
+
+ OnColorChanged(new ColorChangedEventArgs(
+ change.GetOldValue(),
+ change.GetNewValue()));
+
+ ignorePropertyChanged = false;
+ }
+ else if (change.Property == HsvColorProperty)
+ {
+ ignorePropertyChanged = true;
+
+ Color = HsvColor.ToRgb();
+ SetColorToHexTextBox();
+
+ OnColorChanged(new ColorChangedEventArgs(
+ change.GetOldValue().ToRgb(),
+ change.GetNewValue().ToRgb()));
+
+ ignorePropertyChanged = false;
+ }
+ else if (change.Property == PaletteProperty)
+ {
+ IColorPalette? palette = Palette;
+
+ // Any custom palette change must be automatically synced with the
+ // bound properties controlling the palette grid
+ if (palette != null)
+ {
+ PaletteColumnCount = palette.ColorCount;
+
+ List newPaletteColors = new List();
+ for (int shadeIndex = 0; shadeIndex < palette.ShadeCount; shadeIndex++)
+ {
+ for (int colorIndex = 0; colorIndex < palette.ColorCount; colorIndex++)
+ {
+ newPaletteColors.Add(palette.GetColor(colorIndex, shadeIndex));
+ }
+ }
+
+ PaletteColors = newPaletteColors;
+ }
+ }
+ else if (change.Property == IsAlphaEnabledProperty)
+ {
+ // Manually coerce the HsvColor value
+ // (Color will be coerced automatically if HsvColor changes)
+ HsvColor = OnCoerceHsvColor(HsvColor);
+ }
+ else if (change.Property == IsColorComponentsVisibleProperty ||
+ change.Property == IsColorPaletteVisibleProperty ||
+ change.Property == IsColorSpectrumVisibleProperty)
+ {
+ // When the property changed notification is received here the visibility
+ // of individual tab items has not yet been updated through the bindings.
+ // Therefore, the validation is delayed until after bindings update.
+ Dispatcher.UIThread.Post(() =>
+ {
+ ValidateSelection();
+ }, DispatcherPriority.Background);
+ }
+ else if (change.Property == SelectedIndexProperty)
+ {
+ // Again, it is necessary to wait for the SelectedIndex value to
+ // be applied to the TabControl through binding before validation occurs.
+ Dispatcher.UIThread.Post(() =>
+ {
+ ValidateSelection();
+ }, DispatcherPriority.Background);
+ }
+
+ base.OnPropertyChanged(change);
+ }
+
+ ///
+ /// Called before the event occurs.
+ ///
+ /// The defining old/new colors.
+ protected virtual void OnColorChanged(ColorChangedEventArgs e)
+ {
+ ColorChanged?.Invoke(this, e);
+ }
+
+ ///
+ /// Called when the property has to be coerced.
+ ///
+ /// The value to coerce.
+ protected virtual Color OnCoerceColor(Color value)
+ {
+ if (IsAlphaEnabled == false)
+ {
+ return new Color(255, value.R, value.G, value.B);
+ }
+
+ return value;
+ }
+
+ ///
+ /// Called when the property has to be coerced.
+ ///
+ /// The value to coerce.
+ protected virtual HsvColor OnCoerceHsvColor(HsvColor value)
+ {
+ if (IsAlphaEnabled == false)
+ {
+ return new HsvColor(1.0, value.H, value.S, value.V);
+ }
+
+ return value;
+ }
+
+ ///
+ /// Coerces/validates the property value.
+ ///
+ /// The instance.
+ /// The value to coerce.
+ /// The coerced/validated value.
+ private static Color CoerceColor(IAvaloniaObject instance, Color value)
+ {
+ if (instance is ColorView colorView)
+ {
+ return colorView.OnCoerceColor(value);
+ }
+
+ return value;
+ }
+
+ ///
+ /// Coerces/validates the property value.
+ ///
+ /// The instance.
+ /// The value to coerce.
+ /// The coerced/validated value.
+ private static HsvColor CoerceHsvColor(IAvaloniaObject instance, HsvColor value)
+ {
+ if (instance is ColorView colorView)
+ {
+ return colorView.OnCoerceHsvColor(value);
+ }
+
+ return value;
+ }
+
+ ///
+ /// Event handler for when a key is pressed within the Hex RGB value TextBox.
+ /// This is used to trigger re-evaluation of the color based on the TextBox value.
+ ///
+ private void HexTextBox_KeyDown(object? sender, Input.KeyEventArgs e)
+ {
+ if (e.Key == Input.Key.Enter)
+ {
+ GetColorFromHexTextBox();
+ }
+ }
+
+ ///
+ /// Event handler for when the Hex RGB value TextBox looses focus.
+ /// This is used to trigger re-evaluation of the color based on the TextBox value.
+ ///
+ private void HexTextBox_LostFocus(object? sender, Interactivity.RoutedEventArgs e)
+ {
+ GetColorFromHexTextBox();
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs
new file mode 100644
index 00000000000..582653e295a
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs
@@ -0,0 +1,26 @@
+namespace Avalonia.Controls
+{
+ ///
+ /// Defines a specific tab/page (subview) within the .
+ ///
+ ///
+ /// This is indexed to match the default control template ordering.
+ ///
+ public enum ColorViewTab
+ {
+ ///
+ /// The color spectrum subview with a box/ring spectrum and sliders.
+ ///
+ Spectrum = 0,
+
+ ///
+ /// The color palette subview with a grid of selectable colors.
+ ///
+ Palette = 1,
+
+ ///
+ /// The components subview with sliders and numeric input boxes.
+ ///
+ Components = 2,
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs
index 4d05222e315..2c8e09adc9a 100644
--- a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs
+++ b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs
@@ -7,8 +7,10 @@ namespace Avalonia.Controls.Primitives.Converters
{
///
/// Creates an accent color for a given base color value and step parameter.
- /// This is a highly-specialized converter for the color picker.
///
+ ///
+ /// This is a highly-specialized converter for the color picker.
+ ///
public class AccentColorConverter : IValueConverter
{
///
diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs
index 9b09073d9dc..8d5f2332be2 100644
--- a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs
+++ b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs
@@ -42,7 +42,7 @@ public class ColorToHexConverter : IValueConverter
return AvaloniaProperty.UnsetValue;
}
- string hexColor = color.ToString();
+ string hexColor = color.ToUint32().ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant();
if (includeSymbol == false)
{
diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs
new file mode 100644
index 00000000000..8b66b1a4e56
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Globalization;
+using Avalonia.Controls.Converters;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Primitives.Converters
+{
+ ///
+ /// Gets a , either black or white, depending on the luminance of the supplied color.
+ /// A default color supplied in the converter parameter may be returned if alpha is below the set threshold.
+ ///
+ ///
+ /// This is a highly-specialized converter for the color picker.
+ ///
+ public class ContrastBrushConverter : IValueConverter
+ {
+ private ToColorConverter toColorConverter = new ToColorConverter();
+
+ ///
+ /// 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,
+ CultureInfo culture)
+ {
+ Color comparisonColor;
+ Color? defaultColor = null;
+
+ // Get the changing color to compare against
+ var convertedValue = toColorConverter.Convert(value, targetType, parameter, culture);
+ if (convertedValue is Color valueColor)
+ {
+ comparisonColor = valueColor;
+ }
+ else
+ {
+ // Invalid color value provided
+ return AvaloniaProperty.UnsetValue;
+ }
+
+ // Get the default color when transparency is high
+ var convertedParameter = toColorConverter.Convert(parameter, targetType, parameter, culture);
+ if (convertedParameter is Color parameterColor)
+ {
+ defaultColor = parameterColor;
+ }
+
+ if (comparisonColor.A < AlphaThreshold &&
+ defaultColor.HasValue)
+ {
+ // If the transparency is less than the threshold, 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 (ColorHelper.GetRelativeLuminance(comparisonColor) <= 0.5)
+ {
+ // Dark color, return light for contrast
+ return new SolidColorBrush(Colors.White);
+ }
+ else
+ {
+ // Bright color, return dark for contrast
+ return new SolidColorBrush(Colors.Black);
+ }
+ }
+ }
+
+ ///
+ public object? ConvertBack(
+ object? value,
+ Type targetType,
+ object? parameter,
+ CultureInfo culture)
+ {
+ return AvaloniaProperty.UnsetValue;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs
index 220a993f99d..11e33c74f0d 100644
--- a/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs
+++ b/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs
@@ -7,8 +7,10 @@ namespace Avalonia.Controls.Primitives.Converters
///
/// Gets the third corresponding with a given
/// that represents the other two components.
- /// This is a highly-specialized converter for the color picker.
///
+ ///
+ /// This is a highly-specialized converter for the color picker.
+ ///
public class ThirdComponentConverter : IValueConverter
{
///
diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs b/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs
deleted file mode 100644
index 2710c220f41..00000000000
--- a/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using Avalonia.Data.Converters;
-
-namespace Avalonia.Controls.Primitives.Converters
-{
- ///
- /// Converter to chain together multiple converters.
- ///
- public class ValueConverterGroup : List, IValueConverter
- {
- ///
- ///
- public object? Convert(
- object? value,
- Type targetType,
- object? parameter,
- CultureInfo culture)
- {
- object? curValue;
-
- curValue = value;
- for (int i = 0; i < Count; i++)
- {
- curValue = this[i].Convert(curValue, targetType, parameter, culture);
- }
-
- return curValue;
- }
-
- ///
- public object? ConvertBack(
- object? value,
- Type targetType,
- object? parameter,
- CultureInfo culture)
- {
- object? curValue;
-
- curValue = value;
- for (int i = (Count - 1); i >= 0; i--)
- {
- curValue = this[i].ConvertBack(curValue, targetType, parameter, culture);
- }
-
- return curValue;
- }
- }
-}
diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs
index 32a898ee715..7dc340ea16f 100644
--- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs
+++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs
@@ -64,6 +64,10 @@ public static string ToDisplayName(Color color)
// It is also needlessly large as there are only ~140 known/named colors.
// Therefore, rounding of the input color's component values is done to
// reduce the color space into something more useful.
+ //
+ // The rounding value of 5 is specially chosen.
+ // It is a factor of 255 and therefore evenly divisible which improves
+ // the quality of the calculations.
double rounding = 5;
var roundedColor = new Color(
0xFF,
diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml
index 15e5ca16554..c3bc7df4a43 100644
--- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml
+++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml
@@ -1,84 +1,100 @@
-
-
-
-
+
+ 80
+ 40
diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml
index 19f10201a5a..35cd7a9faae 100644
--- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml
+++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml
@@ -1,13 +1,7 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml
index cb764a738cb..74f33d1258c 100644
--- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml
+++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml
@@ -1,84 +1,100 @@
-
-
-
-
+
+ 80
+ 40
diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml
index 18a081721a5..162ac372de8 100644
--- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml
+++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml
@@ -1,13 +1,7 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml
index c25d79727fc..186b6de9bc5 100644
--- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml
+++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml
@@ -1,8 +1,10 @@
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:converters="using:Avalonia.Controls.Converters">
-
+
@@ -18,6 +20,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -25,4 +37,8 @@
+
+
+
+
diff --git a/src/Avalonia.Controls/Converters/EnumToBoolConverter.cs b/src/Avalonia.Controls/Converters/EnumToBoolConverter.cs
new file mode 100644
index 00000000000..ed3065809ee
--- /dev/null
+++ b/src/Avalonia.Controls/Converters/EnumToBoolConverter.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Globalization;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+
+namespace Avalonia.Controls.Converters
+{
+ ///
+ /// Converter to convert an enum value to bool by comparing to the given parameter.
+ /// Both value and parameter must be of the same enum type.
+ ///
+ ///
+ /// This converter is useful to enable binding of radio buttons with a selected enum value.
+ ///
+ public class EnumToBoolConverter : IValueConverter
+ {
+ ///
+ public object? Convert(
+ object? value,
+ Type targetType,
+ object? parameter,
+ CultureInfo culture)
+ {
+ if (value == null &&
+ parameter == null)
+ {
+ return true;
+ }
+ else if (value == null ||
+ parameter == null)
+ {
+ return false;
+ }
+ else
+ {
+ return value!.Equals(parameter);
+ }
+ }
+
+ ///
+ public object? ConvertBack(
+ object? value,
+ Type targetType,
+ object? parameter,
+ CultureInfo culture)
+ {
+ if (value is bool boolValue &&
+ boolValue == true)
+ {
+ return parameter;
+ }
+
+ return BindingOperations.DoNothing;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs
deleted file mode 100644
index 1a33a82ca44..00000000000
--- a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System;
-using System.Globalization;
-using Avalonia.Data.Converters;
-
-namespace Avalonia.Controls.Converters
-{
- ///
- /// Converter that checks if an enum value is equal to the given parameter enum value.
- ///
- public class EnumValueEqualsConverter : IValueConverter
- {
- ///
- public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
- {
- // Note: Unlike string comparisons, null/empty is not supported
- // Both 'value' and 'parameter' must exist and if both are missing they are not considered equal
- if (value != null &&
- parameter != null)
- {
- Type type = value.GetType();
-
- if (type.IsEnum)
- {
- var valueStr = value?.ToString();
- var paramStr = parameter?.ToString();
-
- if (string.Equals(valueStr, paramStr, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
-
- /*
- // TODO: When .net Standard 2.0 is no longer supported the code can be changed to below
- // This is a little more type safe
- if (type.IsEnum &&
- Enum.TryParse(type, value?.ToString(), true, out object? valueEnum) &&
- Enum.TryParse(type, parameter?.ToString(), true, out object? paramEnum))
- {
- return valueEnum == paramEnum;
- }
- */
- }
-
- return false;
- }
-
- ///
- public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
- {
- throw new System.NotImplementedException();
- }
- }
-}
diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs
index e50f991cdbe..35033c58f0d 100644
--- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs
+++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs
@@ -291,7 +291,9 @@ public sealed override void ApplyTemplate()
// Existing code kinda expect to see a NameScope even if it's empty
if (nameScope == null)
+ {
nameScope = new NameScope();
+ }
var e = new TemplateAppliedEventArgs(nameScope);
OnApplyTemplate(e);