Browse Source

Merge pull request #8215 from robloo/colorpicker2

Add new ColorView & ColorPicker Controls
Max Katz 3 years ago
parent
commit
98ca5480f1
34 changed files with 2944 additions and 420 deletions
  1. 11 33
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  2. 284 0
      src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs
  3. 136 0
      src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs
  4. 38 0
      src/Avalonia.Controls.ColorPicker/ColorPalettes/IColorPalette.cs
  5. 302 0
      src/Avalonia.Controls.ColorPicker/ColorPalettes/SixteenColorPalette.cs
  6. 19 0
      src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs
  7. 7 7
      src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs
  8. 27 26
      src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs
  9. 10 10
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs
  10. 76 33
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
  11. 1 1
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
  12. 0 3
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
  13. 495 0
      src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs
  14. 379 0
      src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs
  15. 26 0
      src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs
  16. 3 1
      src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs
  17. 1 1
      src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs
  18. 87 0
      src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs
  19. 3 1
      src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs
  20. 0 50
      src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs
  21. 4 0
      src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs
  22. 76 60
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml
  23. 12 18
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml
  24. 14 20
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml
  25. 14 2
      src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml
  26. 91 0
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  27. 76 60
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml
  28. 12 18
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml
  29. 14 20
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml
  30. 650 0
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  31. 18 2
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml
  32. 56 0
      src/Avalonia.Controls/Converters/EnumToBoolConverter.cs
  33. 0 54
      src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs
  34. 2 0
      src/Avalonia.Controls/Primitives/TemplatedControl.cs

+ 11 - 33
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@@ -13,8 +13,14 @@
     <pc:ThirdComponentConverter x:Key="ThirdComponent" />
   </UserControl.Resources>
 
-  <Grid ColumnDefinitions="Auto,10,Auto">
-    <Grid Grid.Column="0"
+  <Grid ColumnDefinitions="Auto,10,Auto,10,Auto"
+        RowDefinitions="Auto,Auto">
+    <ColorPicker Grid.Column="0"
+                 Grid.Row="1" />
+    <ColorView Grid.Column="0"
+               Grid.Row="0"
+               ColorSpectrumShape="Ring" />
+    <Grid Grid.Column="2"
           Grid.Row="0"
           RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
       <ColorSpectrum x:Name="ColorSpectrum1"
@@ -41,39 +47,11 @@
                    ColorModel="Hsva"
                    HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
       <ColorPreviewer Grid.Row="5"
-                      ShowAccentColors="True"
+                      IsAccentColorsVisible="True"
                       HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
     </Grid>
-    <Grid Grid.Column="2"
-          Grid.Row="0"
-          ColumnDefinitions="Auto,Auto,Auto"
-          RowDefinitions="Auto,Auto">
-      <ColorSlider Grid.Column="0"
-                   Grid.Row="0"
-                   IsAlphaMaxForced="True"
-                   IsSaturationValueMaxForced="False"
-                   ColorComponent="{Binding Components, ElementName=ColorSpectrum2, Converter={StaticResource ThirdComponent}}"
-                   ColorModel="Hsva"
-                   Orientation="Vertical"
-                   HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
-      <ColorSpectrum x:Name="ColorSpectrum2"
-                     Grid.Column="1"
-                     Grid.Row="0"
-                     Color="Green"
-                     Shape="Ring"
-                     Height="256"
-                     Width="256" />
-      <ColorSlider Grid.Column="2"
-                   Grid.Row="0"
-                   ColorComponent="Alpha"
-                   ColorModel="Hsva"
-                   Orientation="Vertical"
-                   HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
-      <ColorPreviewer Grid.Column="0"
-                      Grid.ColumnSpan="3"
-                      Grid.Row="1"
-                      ShowAccentColors="True"
-                      HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
+    <Grid Grid.Column="4"
+          Grid.Row="0">
     </Grid>
   </Grid>
 </UserControl>

+ 284 - 0
src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs

@@ -0,0 +1,284 @@
+using Avalonia.Media;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Implements a reduced flat design or flat UI color palette.
+    /// </summary>
+    /// <remarks>
+    /// 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.
+    ///
+    /// </remarks>
+    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),
+            },
+        };
+
+        /// <summary>
+        /// Gets the index of the default shade of colors in this palette.
+        /// </summary>
+        public const int DefaultShadeIndex = 2;
+
+        /// <summary>
+        /// The index in the color palette of the 'Pomegranate' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int PomegranateIndex = 0;
+
+        /// <summary>
+        /// The index in the color palette of the 'Amethyst' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int AmethystIndex = 1;
+
+        /// <summary>
+        /// The index in the color palette of the 'BelizeHole' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int BelizeHoleIndex = 2;
+
+        /// <summary>
+        /// The index in the color palette of the 'Turquoise' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int TurquoiseIndex = 3;
+
+        /// <summary>
+        /// The index in the color palette of the 'Nephritis' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int NephritisIndex = 4;
+
+        /// <summary>
+        /// The index in the color palette of the 'Sunflower' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int SunflowerIndex = 5;
+
+        /// <summary>
+        /// The index in the color palette of the 'Carrot' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int CarrotIndex = 6;
+
+        /// <summary>
+        /// The index in the color palette of the 'Clouds' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int CloudsIndex = 7;
+
+        /// <summary>
+        /// The index in the color palette of the 'Concrete' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int ConcreteIndex = 8;
+
+        /// <summary>
+        /// The index in the color palette of the 'WetAsphalt' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int WetAsphaltIndex = 9;
+
+        /// <inheritdoc/>
+        public int ColorCount
+        {
+            // Table is transposed compared to the reference chart
+            get => colorChart.GetLength(0);
+        }
+
+        /// <inheritdoc/>
+        public int ShadeCount
+        {
+            // Table is transposed compared to the reference chart
+            get => colorChart.GetLength(1);
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FFC0392B.
+        /// </summary>
+        public static Color Pomegranate
+        {
+            get => colorChart[PomegranateIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF9B59B6.
+        /// </summary>
+        public static Color Amethyst
+        {
+            get => colorChart[AmethystIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF2980B9.
+        /// </summary>
+        public static Color BelizeHole
+        {
+            get => colorChart[BelizeHoleIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF1ABC9C.
+        /// </summary>
+        public static Color Turquoise
+        {
+            get => colorChart[TurquoiseIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF27AE60.
+        /// </summary>
+        public static Color Nephritis
+        {
+            get => colorChart[NephritisIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FFF1C40F.
+        /// </summary>
+        public static Color Sunflower
+        {
+            get => colorChart[SunflowerIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FFE67E22.
+        /// </summary>
+        public static Color Carrot
+        {
+            get => colorChart[CarrotIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FFECF0F1.
+        /// </summary>
+        public static Color Clouds
+        {
+            get => colorChart[CloudsIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF95A5A6.
+        /// </summary>
+        public static Color Concrete
+        {
+            get => colorChart[ConcreteIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF34495E.
+        /// </summary>
+        public static Color WetAsphalt
+        {
+            get => colorChart[WetAsphaltIndex, DefaultShadeIndex];
+        }
+
+        /// <inheritdoc/>
+        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)];
+        }
+    }
+}

+ 136 - 0
src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs

@@ -0,0 +1,136 @@
+using Avalonia.Media;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Implements the standard Windows 10 color palette.
+    /// </summary>
+    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 */
+            }
+        };
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        /// <inheritdoc path="/remarks"/>
+        public int ColorCount
+        {
+            get => colorChart.GetLength(0);
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        /// <inheritdoc path="/remarks"/>
+        public int ShadeCount
+        {
+            get => colorChart.GetLength(1);
+        }
+
+        /// <inheritdoc/>
+        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)];
+        }
+    }
+}

+ 38 - 0
src/Avalonia.Controls.ColorPicker/ColorPalettes/IColorPalette.cs

@@ -0,0 +1,38 @@
+using Avalonia.Media;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Interface to define a color palette.
+    /// </summary>
+    public interface IColorPalette
+    {
+        /// <summary>
+        /// Gets the total number of colors in this palette.
+        /// A color is not necessarily a single value and may be composed of several shades.
+        /// </summary>
+        /// <remarks>
+        /// Represents total columns in a table.
+        /// </remarks>
+        int ColorCount { get; }
+
+        /// <summary>
+        /// Gets the total number of shades for each color in this palette.
+        /// Shades are usually a variation of the color lightening or darkening it.
+        /// </summary>
+        /// <remarks>
+        /// Represents total rows in a table.
+        /// </remarks>
+        int ShadeCount { get; }
+
+        /// <summary>
+        /// Gets a color in the palette by index.
+        /// </summary>
+        /// <param name="colorIndex">The index of the color in the palette.
+        /// The index must be between zero and <see cref="ColorCount"/>.</param>
+        /// <param name="shadeIndex">The index of the color shade in the palette.
+        /// The index must be between zero and <see cref="ShadeCount"/>.</param>
+        /// <returns>The color at the specified index or an exception.</returns>
+        Color GetColor(int colorIndex, int shadeIndex);
+    }
+}

+ 302 - 0
src/Avalonia.Controls.ColorPicker/ColorPalettes/SixteenColorPalette.cs

@@ -0,0 +1,302 @@
+using Avalonia.Media;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Implements the standard sixteen color palette from the HTML 4.01 specification.
+    /// </summary>
+    /// <remarks>
+    /// See https://en.wikipedia.org/wiki/Web_colors#HTML_color_names.
+    /// </remarks>
+    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
+            }
+        };
+
+        /// <summary>
+        /// Gets the index of the default shade of colors in this palette.
+        /// </summary>
+        public const int DefaultShadeIndex = 0;
+
+        /// <summary>
+        /// The index in the color palette of the 'White' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int WhiteIndex = 0;
+
+        /// <summary>
+        /// The index in the color palette of the 'Silver' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int SilverIndex = 1;
+
+        /// <summary>
+        /// The index in the color palette of the 'Gray' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int GrayIndex = 2;
+
+        /// <summary>
+        /// The index in the color palette of the 'Black' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int BlackIndex = 3;
+
+        /// <summary>
+        /// The index in the color palette of the 'Red' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int RedIndex = 4;
+
+        /// <summary>
+        /// The index in the color palette of the 'Maroon' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int MaroonIndex = 5;
+
+        /// <summary>
+        /// The index in the color palette of the 'Yellow' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int YellowIndex = 6;
+
+        /// <summary>
+        /// The index in the color palette of the 'Olive' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int OliveIndex = 7;
+
+        /// <summary>
+        /// The index in the color palette of the 'Lime' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int LimeIndex = 8;
+
+        /// <summary>
+        /// The index in the color palette of the 'Green' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int GreenIndex = 9;
+
+        /// <summary>
+        /// The index in the color palette of the 'Aqua' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int AquaIndex = 10;
+
+        /// <summary>
+        /// The index in the color palette of the 'Teal' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int TealIndex = 11;
+
+        /// <summary>
+        /// The index in the color palette of the 'Blue' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int BlueIndex = 12;
+
+        /// <summary>
+        /// The index in the color palette of the 'Navy' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int NavyIndex = 13;
+
+        /// <summary>
+        /// The index in the color palette of the 'Fuchsia' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int FuchsiaIndex = 14;
+
+        /// <summary>
+        /// The index in the color palette of the 'Purple' color.
+        /// This index can correspond to multiple color shades.
+        /// </summary>
+        public const int PurpleIndex = 15;
+
+        /// <inheritdoc/>
+        public int ColorCount
+        {
+            get => colorChart.GetLength(0);
+        }
+
+        /// <inheritdoc/>
+        public int ShadeCount
+        {
+            get => colorChart.GetLength(1);
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FFFFFFFF.
+        /// </summary>
+        public static Color White
+        {
+            get => colorChart[WhiteIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FFC0C0C0.
+        /// </summary>
+        public static Color Silver
+        {
+            get => colorChart[SilverIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF808080.
+        /// </summary>
+        public static Color Gray
+        {
+            get => colorChart[GrayIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF000000.
+        /// </summary>
+        public static Color Black
+        {
+            get => colorChart[BlackIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FFFF0000.
+        /// </summary>
+        public static Color Red
+        {
+            get => colorChart[RedIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF800000.
+        /// </summary>
+        public static Color Maroon
+        {
+            get => colorChart[MaroonIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FFFFFF00.
+        /// </summary>
+        public static Color Yellow
+        {
+            get => colorChart[YellowIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF808000.
+        /// </summary>
+        public static Color Olive
+        {
+            get => colorChart[OliveIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF00FF00.
+        /// </summary>
+        public static Color Lime
+        {
+            get => colorChart[LimeIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF008000.
+        /// </summary>
+        public static Color Green
+        {
+            get => colorChart[GreenIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF00FFFF.
+        /// </summary>
+        public static Color Aqua
+        {
+            get => colorChart[AquaIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF008080.
+        /// </summary>
+        public static Color Teal
+        {
+            get => colorChart[TealIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF0000FF.
+        /// </summary>
+        public static Color Blue
+        {
+            get => colorChart[BlueIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF000080.
+        /// </summary>
+        public static Color Navy
+        {
+            get => colorChart[NavyIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FFFF00FF.
+        /// </summary>
+        public static Color Fuchsia
+        {
+            get => colorChart[FuchsiaIndex, DefaultShadeIndex];
+        }
+
+        /// <summary>
+        /// Gets the palette defined color that has an ARGB value of #FF800080.
+        /// </summary>
+        public static Color Purple
+        {
+            get => colorChart[PurpleIndex, DefaultShadeIndex];
+        }
+
+        /// <inheritdoc/>
+        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)];
+        }
+    }
+}

+ 19 - 0
src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs

@@ -0,0 +1,19 @@
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// 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.
+    /// </summary>
+    public class ColorPicker : ColorView
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ColorPicker"/> class.
+        /// </summary>
+        public ColorPicker() : base()
+        {
+            // Completely ignore property changes here
+            // The ColorView in the control template is responsible to manage this
+            base.ignorePropertyChanged = true;
+        }
+    }
+}

+ 7 - 7
src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs

@@ -16,11 +16,11 @@ namespace Avalonia.Controls.Primitives
                 defaultBindingMode: BindingMode.TwoWay);
 
         /// <summary>
-        /// Defines the <see cref="ShowAccentColors"/> property.
+        /// Defines the <see cref="IsAccentColorsVisible"/> property.
         /// </summary>
-        public static readonly StyledProperty<bool> ShowAccentColorsProperty =
+        public static readonly StyledProperty<bool> IsAccentColorsVisibleProperty =
             AvaloniaProperty.Register<ColorPreviewer, bool>(
-                nameof(ShowAccentColors),
+                nameof(IsAccentColorsVisible),
                 true);
 
         /// <summary>
@@ -38,13 +38,13 @@ namespace Avalonia.Controls.Primitives
         }
 
         /// <summary>
-        /// 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.
         /// </summary>
-        public bool ShowAccentColors
+        public bool IsAccentColorsVisible
         {
-            get => GetValue(ShowAccentColorsProperty);
-            set => SetValue(ShowAccentColorsProperty, value);
+            get => GetValue(IsAccentColorsVisibleProperty);
+            set => SetValue(IsAccentColorsVisibleProperty, value);
         }
     }
 }

+ 27 - 26
src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs

@@ -10,10 +10,10 @@ namespace Avalonia.Controls.Primitives
     /// <summary>
     /// Presents a preview color with optional accent colors.
     /// </summary>
-    [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
     {
         /// <summary>
@@ -24,10 +24,11 @@ namespace Avalonia.Controls.Primitives
 
         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;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ColorPreviewer"/> class.
@@ -45,20 +46,20 @@ namespace Avalonia.Controls.Primitives
             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 @@ namespace Avalonia.Controls.Primitives
             // Remove any existing events present if the control was previously loaded then unloaded
             ConnectEvents(false);
 
-            AccentDec1Border = e.NameScope.Find<Border>(nameof(AccentDec1Border));
-            AccentDec2Border = e.NameScope.Find<Border>(nameof(AccentDec2Border));
-            AccentInc1Border = e.NameScope.Find<Border>(nameof(AccentInc1Border));
-            AccentInc2Border = e.NameScope.Find<Border>(nameof(AccentInc2Border));
+            _accentDecrement1Border = e.NameScope.Find<Border>("PART_AccentDecrement1Border");
+            _accentDecrement2Border = e.NameScope.Find<Border>("PART_AccentDecrement2Border");
+            _accentIncrement1Border = e.NameScope.Find<Border>("PART_AccentIncrement1Border");
+            _accentIncrement2Border = e.NameScope.Find<Border>("PART_AccentIncrement2Border");
 
             // Must connect after controls are found
             ConnectEvents(true);
@@ -116,15 +117,15 @@ namespace Avalonia.Controls.Primitives
             // 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);
+            }
         }
     }
 }

+ 10 - 10
src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs

@@ -49,12 +49,12 @@ namespace Avalonia.Controls.Primitives
                 true);
 
         /// <summary>
-        /// Defines the <see cref="IsAutoUpdatingEnabled"/> property.
+        /// Defines the <see cref="IsRoundingEnabled"/> property.
         /// </summary>
-        public static readonly StyledProperty<bool> IsAutoUpdatingEnabledProperty =
+        public static readonly StyledProperty<bool> IsRoundingEnabledProperty =
             AvaloniaProperty.Register<ColorSlider, bool>(
-                nameof(IsAutoUpdatingEnabled),
-                true);
+                nameof(IsRoundingEnabled),
+                false);
 
         /// <summary>
         /// Defines the <see cref="IsSaturationValueMaxForced"/> property.
@@ -120,16 +120,16 @@ namespace Avalonia.Controls.Primitives
         }
 
         /// <summary>
-        /// 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.
         /// </summary>
         /// <remarks>
-        /// This can be disabled for performance reasons when working with multiple sliders.
+        /// This is applicable for the HSV color model only. The <see cref="Media.HsvColor"/> struct uses double
+        /// values while the <see cref="Media.Color"/> struct uses byte. Only double types need rounding.
         /// </remarks>
-        public bool IsAutoUpdatingEnabled
+        public bool IsRoundingEnabled
         {
-            get => GetValue(IsAutoUpdatingEnabledProperty);
-            set => SetValue(IsAutoUpdatingEnabledProperty, value);
+            get => GetValue(IsRoundingEnabledProperty);
+            set => SetValue(IsRoundingEnabledProperty, value);
         }
 
         /// <summary>

+ 76 - 33
src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs

@@ -20,8 +20,16 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public event EventHandler<ColorChangedEventArgs>? ColorChanged;
 
-        private const double MaxHue = 359.99999999999999999; // 17 decimal places
-        private bool disableUpdates = false;
+        /// <summary>
+        /// Defines the maximum hue component value
+        /// (other components are always 0..100 or 0.255).
+        /// </summary>
+        /// <remarks>
+        /// This should match the default <see cref="ColorSpectrum.MaxHue"/> property.
+        /// </remarks>
+        private const double MaxHue = 359;
+
+        protected bool ignorePropertyChanged = false;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ColorSlider"/> class.
@@ -107,21 +115,41 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
+        /// <summary>
+        /// Rounds the component values of the given <see cref="HsvColor"/>.
+        /// This is useful for user-display and to ensure a color matches user selection exactly.
+        /// </summary>
+        /// <param name="hsvColor">The <see cref="HsvColor"/> to round component values for.</param>
+        /// <returns>A new <see cref="HsvColor"/> with rounded component values.</returns>
+        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));
+        }
+
         /// <summary>
         /// Updates the slider property values by applying the current color.
         /// </summary>
         /// <remarks>
         /// Warning: This will trigger property changed updates.
-        /// Consider using <see cref="disableUpdates"/> externally.
+        /// Consider using <see cref="ignorePropertyChanged"/> externally.
         /// </remarks>
         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 @@ namespace Avalonia.Controls.Primitives
             }
             else
             {
+                var rgbColor = Color;
+
                 switch (component)
                 {
                     case ColorComponent.Alpha:
@@ -183,13 +213,12 @@ namespace Avalonia.Controls.Primitives
             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 @@ namespace Avalonia.Controls.Primitives
                     }
                 }
 
-                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 @@ namespace Avalonia.Controls.Primitives
                         break;
                 }
 
-                return (rgbColor, rgbColor.ToHsv());
+                hsvColor = rgbColor.ToHsv();
             }
+
+            if (IsRoundingEnabled)
+            {
+                hsvColor = RoundComponentValues(hsvColor);
+            }
+
+            return (rgbColor, hsvColor);
         }
 
         /// <summary>
@@ -306,7 +344,7 @@ namespace Avalonia.Controls.Primitives
         /// <inheritdoc/>
         protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
         {
-            if (disableUpdates)
+            if (ignorePropertyChanged)
             {
                 base.OnPropertyChanged(change);
                 return;
@@ -315,54 +353,59 @@ namespace Avalonia.Controls.Primitives
             // 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<Color>(),
                     change.GetNewValue<Color>()));
 
-                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<HsvColor>().ToRgb(),
                     change.GetNewValue<HsvColor>().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 @@ namespace Avalonia.Controls.Primitives
                 UpdatePseudoClasses();
                 OnColorChanged(new ColorChangedEventArgs(oldColor, Color));
 
-                disableUpdates = false;
+                ignorePropertyChanged = false;
             }
 
             base.OnPropertyChanged(change);

+ 1 - 1
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs

@@ -97,7 +97,7 @@ namespace Avalonia.Controls.Primitives
         /// Gets or sets the currently selected color in the RGB color model.
         /// </summary>
         /// <remarks>
-        /// For control authors use <see cref="HsvColor"/> instead to avoid loss
+        /// For control authors, use <see cref="HsvColor"/> instead to avoid loss
         /// of precision and color drifting.
         /// </remarks>
         public Color Color

+ 0 - 3
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs

@@ -44,7 +44,6 @@ namespace Avalonia.Controls.Primitives
 
         private bool _updatingColor = false;
         private bool _updatingHsvColor = false;
-        private bool _isPointerOver = false;
         private bool _isPointerPressed = false;
         private bool _shouldShowLargeSelection = false;
         private List<Hsv> _hsvValues = new List<Hsv>();
@@ -851,7 +850,6 @@ namespace Avalonia.Controls.Primitives
         /// <inheritdoc cref="InputElement.PointerEntered"/>
         private void InputTarget_PointerEntered(object? sender, PointerEventArgs args)
         {
-            _isPointerOver = true;
             UpdatePseudoClasses();
             args.Handled = true;
         }
@@ -859,7 +857,6 @@ namespace Avalonia.Controls.Primitives
         /// <inheritdoc cref="InputElement.PointerExited"/>
         private void InputTarget_PointerExited(object? sender, PointerEventArgs args)
         {
-            _isPointerOver = false;
             UpdatePseudoClasses();
             args.Handled = true;
         }

+ 495 - 0
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
+{
+    /// <inheritdoc/>
+    public partial class ColorView
+    {
+        /// <summary>
+        /// Defines the <see cref="Color"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Color> ColorProperty =
+            AvaloniaProperty.Register<ColorView, Color>(
+                nameof(Color),
+                Colors.White,
+                defaultBindingMode: BindingMode.TwoWay,
+                coerce: CoerceColor) ;
+
+        /// <summary>
+        /// Defines the <see cref="ColorModel"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ColorModel> ColorModelProperty =
+            AvaloniaProperty.Register<ColorView, ColorModel>(
+                nameof(ColorModel),
+                ColorModel.Rgba);
+
+        /// <summary>
+        /// Defines the <see cref="ColorSpectrumComponents"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ColorSpectrumComponents> ColorSpectrumComponentsProperty =
+            AvaloniaProperty.Register<ColorView, ColorSpectrumComponents>(
+                nameof(ColorSpectrumComponents),
+                ColorSpectrumComponents.HueSaturation);
+
+        /// <summary>
+        /// Defines the <see cref="ColorSpectrumShape"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ColorSpectrumShape> ColorSpectrumShapeProperty =
+            AvaloniaProperty.Register<ColorView, ColorSpectrumShape>(
+                nameof(ColorSpectrumShape),
+                ColorSpectrumShape.Box);
+
+        /// <summary>
+        /// Defines the <see cref="HsvColor"/> property.
+        /// </summary>
+        public static readonly StyledProperty<HsvColor> HsvColorProperty =
+            AvaloniaProperty.Register<ColorView, HsvColor>(
+                nameof(HsvColor),
+                Colors.White.ToHsv(),
+                defaultBindingMode: BindingMode.TwoWay,
+                coerce: CoerceHsvColor);
+
+        /// <summary>
+        /// Defines the <see cref="IsAccentColorsVisible"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsAccentColorsVisibleProperty =
+            AvaloniaProperty.Register<ColorView, bool>(
+                nameof(IsAccentColorsVisible),
+                true);
+
+        /// <summary>
+        /// Defines the <see cref="IsAlphaEnabled"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsAlphaEnabledProperty =
+            AvaloniaProperty.Register<ColorView, bool>(
+                nameof(IsAlphaEnabled),
+                true);
+
+        /// <summary>
+        /// Defines the <see cref="IsAlphaVisible"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsAlphaVisibleProperty =
+            AvaloniaProperty.Register<ColorView, bool>(
+                nameof(IsAlphaVisible),
+                true);
+
+        /// <summary>
+        /// Defines the <see cref="IsColorComponentsVisible"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsColorComponentsVisibleProperty =
+            AvaloniaProperty.Register<ColorView, bool>(
+                nameof(IsColorComponentsVisible),
+                true);
+
+        /// <summary>
+        /// Defines the <see cref="IsColorModelVisible"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsColorModelVisibleProperty =
+            AvaloniaProperty.Register<ColorView, bool>(
+                nameof(IsColorModelVisible),
+                true);
+
+        /// <summary>
+        /// Defines the <see cref="IsColorPaletteVisible"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsColorPaletteVisibleProperty =
+            AvaloniaProperty.Register<ColorView, bool>(
+                nameof(IsColorPaletteVisible),
+                true);
+
+        /// <summary>
+        /// Defines the <see cref="IsColorPreviewVisible"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsColorPreviewVisibleProperty =
+            AvaloniaProperty.Register<ColorView, bool>(
+                nameof(IsColorPreviewVisible),
+                true);
+
+        /// <summary>
+        /// Defines the <see cref="IsColorSpectrumVisible"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsColorSpectrumVisibleProperty =
+            AvaloniaProperty.Register<ColorView, bool>(
+                nameof(IsColorSpectrumVisible),
+                true);
+
+        /// <summary>
+        /// Defines the <see cref="IsColorSpectrumSliderVisible"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsColorSpectrumSliderVisibleProperty =
+            AvaloniaProperty.Register<ColorView, bool>(
+                nameof(IsColorSpectrumSliderVisible),
+                true);
+
+        /// <summary>
+        /// Defines the <see cref="IsComponentSliderVisible"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsComponentSliderVisibleProperty =
+            AvaloniaProperty.Register<ColorView, bool>(
+                nameof(IsComponentSliderVisible),
+                true);
+
+        /// <summary>
+        /// Defines the <see cref="IsComponentTextInputVisible"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsComponentTextInputVisibleProperty =
+            AvaloniaProperty.Register<ColorView, bool>(
+                nameof(IsComponentTextInputVisible),
+                true);
+
+        /// <summary>
+        /// Defines the <see cref="IsHexInputVisible"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsHexInputVisibleProperty =
+            AvaloniaProperty.Register<ColorView, bool>(
+                nameof(IsHexInputVisible),
+                true);
+
+        /// <summary>
+        /// Defines the <see cref="MaxHue"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> MaxHueProperty =
+            AvaloniaProperty.Register<ColorView, int>(
+                nameof(MaxHue),
+                359);
+
+        /// <summary>
+        /// Defines the <see cref="MaxSaturation"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> MaxSaturationProperty =
+            AvaloniaProperty.Register<ColorView, int>(
+                nameof(MaxSaturation),
+                100);
+
+        /// <summary>
+        /// Defines the <see cref="MaxValue"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> MaxValueProperty =
+            AvaloniaProperty.Register<ColorView, int>(
+                nameof(MaxValue),
+                100);
+
+        /// <summary>
+        /// Defines the <see cref="MinHue"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> MinHueProperty =
+            AvaloniaProperty.Register<ColorView, int>(
+                nameof(MinHue),
+                0);
+
+        /// <summary>
+        /// Defines the <see cref="MinSaturation"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> MinSaturationProperty =
+            AvaloniaProperty.Register<ColorView, int>(
+                nameof(MinSaturation),
+                0);
+
+        /// <summary>
+        /// Defines the <see cref="MinValue"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> MinValueProperty =
+            AvaloniaProperty.Register<ColorView, int>(
+                nameof(MinValue),
+                0);
+
+        /// <summary>
+        /// Defines the <see cref="PaletteColors"/> property.
+        /// </summary>
+        public static readonly StyledProperty<IEnumerable<Color>?> PaletteColorsProperty =
+            AvaloniaProperty.Register<ColorView, IEnumerable<Color>?>(
+                nameof(PaletteColors),
+                null);
+
+        /// <summary>
+        /// Defines the <see cref="PaletteColumnCount"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> PaletteColumnCountProperty =
+            AvaloniaProperty.Register<ColorView, int>(
+                nameof(PaletteColumnCount),
+                4);
+
+        /// <summary>
+        /// Defines the <see cref="Palette"/> property.
+        /// </summary>
+        public static readonly StyledProperty<IColorPalette?> PaletteProperty =
+            AvaloniaProperty.Register<ColorView, IColorPalette?>(
+                nameof(Palette),
+                null);
+
+        /// <summary>
+        /// Defines the <see cref="SelectedIndex"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> SelectedIndexProperty =
+            AvaloniaProperty.Register<ColorView, int>(
+                nameof(SelectedIndex),
+                (int)ColorViewTab.Spectrum);
+
+        /// <inheritdoc cref="ColorSpectrum.Color"/>
+        public Color Color
+        {
+            get => GetValue(ColorProperty);
+            set => SetValue(ColorProperty, value);
+        }
+
+        /// <inheritdoc cref="ColorSlider.ColorModel"/>
+        /// <remarks>
+        /// 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.
+        /// </remarks>
+        public ColorModel ColorModel
+        {
+            get => GetValue(ColorModelProperty);
+            set => SetValue(ColorModelProperty, value);
+        }
+
+        /// <inheritdoc cref="ColorSpectrum.Components"/>
+        public ColorSpectrumComponents ColorSpectrumComponents
+        {
+            get => GetValue(ColorSpectrumComponentsProperty);
+            set => SetValue(ColorSpectrumComponentsProperty, value);
+        }
+
+        /// <inheritdoc cref="ColorSpectrum.Shape"/>
+        public ColorSpectrumShape ColorSpectrumShape
+        {
+            get => GetValue(ColorSpectrumShapeProperty);
+            set => SetValue(ColorSpectrumShapeProperty, value);
+        }
+
+        /// <inheritdoc cref="ColorSpectrum.HsvColor"/>
+        public HsvColor HsvColor
+        {
+            get => GetValue(HsvColorProperty);
+            set => SetValue(HsvColorProperty, value);
+        }
+
+        /// <inheritdoc cref="ColorPreviewer.IsAccentColorsVisible"/>
+        public bool IsAccentColorsVisible
+        {
+            get => GetValue(IsAccentColorsVisibleProperty);
+            set => SetValue(IsAccentColorsVisibleProperty, value);
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        public bool IsAlphaEnabled
+        {
+            get => GetValue(IsAlphaEnabledProperty);
+            set => SetValue(IsAlphaEnabledProperty, value);
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        /// <remarks>
+        /// Note that <see cref="IsComponentTextInputVisible"/> also controls the alpha
+        /// component TextBox visibility.
+        /// </remarks>
+        public bool IsAlphaVisible
+        {
+            get => GetValue(IsAlphaVisibleProperty);
+            set => SetValue(IsAlphaVisibleProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the color components tab/panel/page (subview) is visible.
+        /// </summary>
+        public bool IsColorComponentsVisible
+        {
+            get => GetValue(IsColorComponentsVisibleProperty);
+            set => SetValue(IsColorComponentsVisibleProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the active color model indicator/selector is visible.
+        /// </summary>
+        public bool IsColorModelVisible
+        {
+            get => GetValue(IsColorModelVisibleProperty);
+            set => SetValue(IsColorModelVisibleProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the color palette tab/panel/page (subview) is visible.
+        /// </summary>
+        public bool IsColorPaletteVisible
+        {
+            get => GetValue(IsColorPaletteVisibleProperty);
+            set => SetValue(IsColorPaletteVisibleProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the color preview is visible.
+        /// </summary>
+        /// <remarks>
+        /// Note that accent color visibility is controlled separately by
+        /// <see cref="IsAccentColorsVisible"/>.
+        /// </remarks>
+        public bool IsColorPreviewVisible
+        {
+            get => GetValue(IsColorPreviewVisibleProperty);
+            set => SetValue(IsColorPreviewVisibleProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the color spectrum tab/panel/page (subview) is visible.
+        /// </summary>
+        public bool IsColorSpectrumVisible
+        {
+            get => GetValue(IsColorSpectrumVisibleProperty);
+            set => SetValue(IsColorSpectrumVisibleProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the color spectrum's third component slider
+        /// is visible.
+        /// </summary>
+        public bool IsColorSpectrumSliderVisible
+        {
+            get => GetValue(IsColorSpectrumSliderVisibleProperty);
+            set => SetValue(IsColorSpectrumSliderVisibleProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether color component sliders are visible.
+        /// </summary>
+        /// <remarks>
+        /// All color components are controlled by this property but alpha can also be
+        /// controlled with <see cref="IsAlphaVisible"/>.
+        /// </remarks>
+        public bool IsComponentSliderVisible
+        {
+            get => GetValue(IsComponentSliderVisibleProperty);
+            set => SetValue(IsComponentSliderVisibleProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether color component text inputs are visible.
+        /// </summary>
+        /// <remarks>
+        /// All color components are controlled by this property but alpha can also be
+        /// controlled with <see cref="IsAlphaVisible"/>.
+        /// </remarks>
+        public bool IsComponentTextInputVisible
+        {
+            get => GetValue(IsComponentTextInputVisibleProperty);
+            set => SetValue(IsComponentTextInputVisibleProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the hexadecimal color value text input
+        /// is visible.
+        /// </summary>
+        public bool IsHexInputVisible
+        {
+            get => GetValue(IsHexInputVisibleProperty);
+            set => SetValue(IsHexInputVisibleProperty, value);
+        }
+
+        /// <inheritdoc cref="ColorSpectrum.MaxHue"/>
+        public int MaxHue
+        {
+            get => GetValue(MaxHueProperty);
+            set => SetValue(MaxHueProperty, value);
+        }
+
+        /// <inheritdoc cref="ColorSpectrum.MaxSaturation"/>
+        public int MaxSaturation
+        {
+            get => GetValue(MaxSaturationProperty);
+            set => SetValue(MaxSaturationProperty, value);
+        }
+
+        /// <inheritdoc cref="ColorSpectrum.MaxValue"/>
+        public int MaxValue
+        {
+            get => GetValue(MaxValueProperty);
+            set => SetValue(MaxValueProperty, value);
+        }
+
+        /// <inheritdoc cref="ColorSpectrum.MinHue"/>
+        public int MinHue
+        {
+            get => GetValue(MinHueProperty);
+            set => SetValue(MinHueProperty, value);
+        }
+
+        /// <inheritdoc cref="ColorSpectrum.MinSaturation"/>
+        public int MinSaturation
+        {
+            get => GetValue(MinSaturationProperty);
+            set => SetValue(MinSaturationProperty, value);
+        }
+
+        /// <inheritdoc cref="ColorSpectrum.MinValue"/>
+        public int MinValue
+        {
+            get => GetValue(MinValueProperty);
+            set => SetValue(MinValueProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the collection of individual colors in the palette.
+        /// </summary>
+        /// <remarks>
+        /// This is not commonly set manually. Instead, it should be set automatically by
+        /// providing an <see cref="IColorPalette"/> to the <see cref="Palette"/> property.
+        /// <br/><br/>
+        /// Also note that this property is what should be bound in the control template.
+        /// <see cref="Palette"/> is too high-level to use on its own.
+        /// </remarks>
+        public IEnumerable<Color>? PaletteColors
+        {
+            get => GetValue(PaletteColorsProperty);
+            set => SetValue(PaletteColorsProperty, value);
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        /// <remarks>
+        /// This is not commonly set manually. Instead, it should be set automatically by
+        /// providing an <see cref="IColorPalette"/> to the <see cref="Palette"/> property.
+        /// <br/><br/>
+        /// Also note that this property is what should be bound in the control template.
+        /// <see cref="Palette"/> is too high-level to use on its own.
+        /// </remarks>
+        public int PaletteColumnCount
+        {
+            get => GetValue(PaletteColumnCountProperty);
+            set => SetValue(PaletteColumnCountProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the color palette.
+        /// </summary>
+        /// <remarks>
+        /// This will automatically set both <see cref="PaletteColors"/> and
+        /// <see cref="PaletteColumnCount"/> overwriting any existing values.
+        /// </remarks>
+        public IColorPalette? Palette
+        {
+            get => GetValue(PaletteProperty);
+            set => SetValue(PaletteProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the index of the selected tab/panel/page (subview).
+        /// </summary>
+        public int SelectedIndex
+        {
+            get => GetValue(SelectedIndexProperty);
+            set => SetValue(SelectedIndexProperty, value);
+        }
+    }
+}

+ 379 - 0
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
+{
+    /// <summary>
+    /// Presents a color for user editing using a spectrum, palette and component sliders.
+    /// </summary>
+    [TemplatePart("PART_HexTextBox", typeof(TextBox))]
+    [TemplatePart("PART_TabControl", typeof(TabControl))]
+    public partial class ColorView : TemplatedControl
+    {
+        /// <summary>
+        /// Event for when the selected color changes within the slider.
+        /// </summary>
+        public event EventHandler<ColorChangedEventArgs>? ColorChanged;
+
+        // XAML template parts
+        private TextBox?    _hexTextBox;
+        private TabControl? _tabControl;
+
+        private ColorToHexConverter colorToHexConverter = new ColorToHexConverter();
+        protected bool ignorePropertyChanged = false;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ColorView"/> class.
+        /// </summary>
+        public ColorView() : base()
+        {
+        }
+
+        /// <summary>
+        /// Gets the value of the hex TextBox and sets it as the current <see cref="Color"/>.
+        /// If invalid, the TextBox hex text will revert back to the last valid color.
+        /// </summary>
+        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();
+            }
+        }
+
+        /// <summary>
+        /// Sets the current <see cref="Color"/> to the hex TextBox.
+        /// </summary>
+        private void SetColorToHexTextBox()
+        {
+            if (_hexTextBox != null)
+            {
+                _hexTextBox.Text = colorToHexConverter.Convert(Color, typeof(string), null, CultureInfo.CurrentCulture) as string;
+            }
+        }
+
+        /// <summary>
+        /// Validates the tab/panel/page selection taking into account the visibility of each item
+        /// as well as the current selection.
+        /// </summary>
+        /// <remarks>
+        /// Derived controls may re-implement this based on their default style / control template
+        /// and any specialized selection needs.
+        /// </remarks>
+        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<ItemsPresenter>();
+                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;
+        }
+
+        /// <inheritdoc/>
+        protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+        {
+            if (_hexTextBox != null)
+            {
+                _hexTextBox.KeyDown -= HexTextBox_KeyDown;
+                _hexTextBox.LostFocus -= HexTextBox_LostFocus;
+            }
+
+            _hexTextBox = e.NameScope.Find<TextBox>("PART_HexTextBox");
+            _tabControl = e.NameScope.Find<TabControl>("PART_TabControl");
+
+            SetColorToHexTextBox();
+
+            if (_hexTextBox != null)
+            {
+                _hexTextBox.KeyDown += HexTextBox_KeyDown;
+                _hexTextBox.LostFocus += HexTextBox_LostFocus;
+            }
+
+            base.OnApplyTemplate(e);
+            ValidateSelection();
+        }
+
+        /// <inheritdoc/>
+        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<Color>(),
+                    change.GetNewValue<Color>()));
+
+                ignorePropertyChanged = false;
+            }
+            else if (change.Property == HsvColorProperty)
+            {
+                ignorePropertyChanged = true;
+
+                Color = HsvColor.ToRgb();
+                SetColorToHexTextBox();
+
+                OnColorChanged(new ColorChangedEventArgs(
+                    change.GetOldValue<HsvColor>().ToRgb(),
+                    change.GetNewValue<HsvColor>().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<Color> newPaletteColors = new List<Color>();
+                    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);
+        }
+
+        /// <summary>
+        /// Called before the <see cref="ColorChanged"/> event occurs.
+        /// </summary>
+        /// <param name="e">The <see cref="ColorChangedEventArgs"/> defining old/new colors.</param>
+        protected virtual void OnColorChanged(ColorChangedEventArgs e)
+        {
+            ColorChanged?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Color"/> property has to be coerced.
+        /// </summary>
+        /// <param name="value">The value to coerce.</param>
+        protected virtual Color OnCoerceColor(Color value)
+        {
+            if (IsAlphaEnabled == false)
+            {
+                return new Color(255, value.R, value.G, value.B);
+            }
+
+            return value;
+        }
+
+        /// <summary>
+        /// Called when the <see cref="HsvColor"/> property has to be coerced.
+        /// </summary>
+        /// <param name="value">The value to coerce.</param>
+        protected virtual HsvColor OnCoerceHsvColor(HsvColor value)
+        {
+            if (IsAlphaEnabled == false)
+            {
+                return new HsvColor(1.0, value.H, value.S, value.V);
+            }
+
+            return value;
+        }
+
+        /// <summary>
+        /// Coerces/validates the <see cref="Color"/> property value.
+        /// </summary>
+        /// <param name="instance">The <see cref="ColorView"/> instance.</param>
+        /// <param name="value">The value to coerce.</param>
+        /// <returns>The coerced/validated value.</returns>
+        private static Color CoerceColor(IAvaloniaObject instance, Color value)
+        {
+            if (instance is ColorView colorView)
+            {
+                return colorView.OnCoerceColor(value);
+            }
+
+            return value;
+        }
+
+        /// <summary>
+        /// Coerces/validates the <see cref="HsvColor"/> property value.
+        /// </summary>
+        /// <param name="instance">The <see cref="ColorView"/> instance.</param>
+        /// <param name="value">The value to coerce.</param>
+        /// <returns>The coerced/validated value.</returns>
+        private static HsvColor CoerceHsvColor(IAvaloniaObject instance, HsvColor value)
+        {
+            if (instance is ColorView colorView)
+            {
+                return colorView.OnCoerceHsvColor(value);
+            }
+
+            return value;
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        private void HexTextBox_KeyDown(object? sender, Input.KeyEventArgs e)
+        {
+            if (e.Key == Input.Key.Enter)
+            {
+                GetColorFromHexTextBox();
+            }
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        private void HexTextBox_LostFocus(object? sender, Interactivity.RoutedEventArgs e)
+        {
+            GetColorFromHexTextBox();
+        }
+    }
+}

+ 26 - 0
src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs

@@ -0,0 +1,26 @@
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Defines a specific tab/page (subview) within the <see cref="ColorView"/>.
+    /// </summary>
+    /// <remarks>
+    /// This is indexed to match the default control template ordering.
+    /// </remarks>
+    public enum ColorViewTab
+    {
+        /// <summary>
+        /// The color spectrum subview with a box/ring spectrum and sliders.
+        /// </summary>
+        Spectrum = 0,
+
+        /// <summary>
+        /// The color palette subview with a grid of selectable colors.
+        /// </summary>
+        Palette = 1,
+
+        /// <summary>
+        /// The components subview with sliders and numeric input boxes.
+        /// </summary>
+        Components = 2,
+    }
+}

+ 3 - 1
src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs

@@ -7,8 +7,10 @@ namespace Avalonia.Controls.Primitives.Converters
 {
     /// <summary>
     /// Creates an accent color for a given base color value and step parameter.
-    /// This is a highly-specialized converter for the color picker.
     /// </summary>
+    /// <remarks>
+    /// This is a highly-specialized converter for the color picker.
+    /// </remarks>
     public class AccentColorConverter : IValueConverter
     {
         /// <summary>

+ 1 - 1
src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs

@@ -42,7 +42,7 @@ namespace Avalonia.Controls.Converters
                 return AvaloniaProperty.UnsetValue;
             }
 
-            string hexColor = color.ToString();
+            string hexColor = color.ToUint32().ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant();
 
             if (includeSymbol == false)
             {

+ 87 - 0
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
+{
+    /// <summary>
+    /// Gets a <see cref="SolidColorBrush"/>, 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.
+    /// </summary>
+    /// <remarks>
+    /// This is a highly-specialized converter for the color picker.
+    /// </remarks>
+    public class ContrastBrushConverter : IValueConverter
+    {
+        private ToColorConverter toColorConverter = new ToColorConverter();
+
+        /// <summary>
+        /// Gets or sets the alpha channel threshold below which a default color is used instead of black/white.
+        /// </summary>
+        public byte AlphaThreshold { get; set; } = 128;
+
+        /// <inheritdoc/>
+        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);
+                }
+            }
+        }
+
+        /// <inheritdoc/>
+        public object? ConvertBack(
+            object? value,
+            Type targetType,
+            object? parameter,
+            CultureInfo culture)
+        {
+            return AvaloniaProperty.UnsetValue;
+        }
+    }
+}

+ 3 - 1
src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs

@@ -7,8 +7,10 @@ namespace Avalonia.Controls.Primitives.Converters
     /// <summary>
     /// Gets the third <see cref="ColorComponent"/> corresponding with a given
     /// <see cref="ColorSpectrumComponents"/> that represents the other two components.
-    /// This is a highly-specialized converter for the color picker.
     /// </summary>
+    /// <remarks>
+    /// This is a highly-specialized converter for the color picker.
+    /// </remarks>
     public class ThirdComponentConverter : IValueConverter
     {
         /// <inheritdoc/>

+ 0 - 50
src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs

@@ -1,50 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using Avalonia.Data.Converters;
-
-namespace Avalonia.Controls.Primitives.Converters
-{
-    /// <summary>
-    /// Converter to chain together multiple converters.
-    /// </summary>
-    public class ValueConverterGroup : List<IValueConverter>, IValueConverter
-    {
-        /// <inheritdoc/>
-        /// <inheritdoc/>
-        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;
-        }
-
-        /// <inheritdoc/>
-        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;
-        }
-    }
-}

+ 4 - 0
src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs

@@ -64,6 +64,10 @@ namespace Avalonia.Controls.Primitives
             // 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,

+ 76 - 60
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml

@@ -1,84 +1,100 @@
 <Styles xmlns="https://github.com/avaloniaui"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:converters="using:Avalonia.Controls.Converters"
         xmlns:pc="using:Avalonia.Controls.Primitives.Converters"
         x:CompileBindings="True">
 
   <Styles.Resources>
-    <pc:AccentColorConverter x:Key="AccentColor" />
-    <converters:ToBrushConverter x:Key="ToBrush" />
-    <converters:CornerRadiusFilterConverter x:Key="RightCornerRadiusFilterConverter" Filter="TopRight, BottomRight"/>
-    <converters:CornerRadiusFilterConverter x:Key="LeftCornerRadiusFilterConverter" Filter="TopLeft, BottomLeft"/>
+    <pc:AccentColorConverter x:Key="AccentColorConverter" />
+    <x:Double x:Key="ColorPreviewerAccentSectionWidth">80</x:Double>
+    <x:Double x:Key="ColorPreviewerAccentSectionHeight">40</x:Double>
   </Styles.Resources>
 
   <Style Selector="ColorPreviewer">
     <Setter Property="Height" Value="70" />
     <Setter Property="CornerRadius" Value="0" />
     <Setter Property="Template">
-      <ControlTemplate>
-        <Grid ColumnDefinitions="Auto,*,Auto">
-          <!-- Left accent colors -->
-          <Grid Grid.Column="0"
-                Height="40"
-                Width="80"
-                ColumnDefinitions="*,*"
-                Margin="0,0,-10,0"
-                VerticalAlignment="Center"
-                IsVisible="{TemplateBinding ShowAccentColors}">
-            <Border Grid.Column="0"
-                    Grid.ColumnSpan="2"
-                    HorizontalAlignment="Stretch"
-                    VerticalAlignment="Stretch"
-                    Background="{StaticResource CheckeredBackgroundBrush}" />
-            <Border x:Name="AccentDec2Border"
-                    Grid.Column="0"
-                    CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}"
-                    Tag="-2"
-                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-2'}" />
-            <Border x:Name="AccentDec1Border"
-                    Grid.Column="1"
-                    Tag="-1"
-                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-1'}" />
-          </Grid>
-          <!-- Right accent colors -->
-          <Grid Grid.Column="2"
-                Height="40"
-                Width="80"
-                ColumnDefinitions="*,*"
-                Margin="-10,0,0,0"
-                VerticalAlignment="Center"
-                IsVisible="{TemplateBinding ShowAccentColors}">
-            <Border Grid.Column="0"
-                    Grid.ColumnSpan="2"
+      <ControlTemplate TargetType="{x:Type ColorPreviewer}">
+        <Panel>
+          <!-- Preview color with accents to the left and right -->
+          <Grid ColumnDefinitions="Auto,*,Auto"
+                IsVisible="{TemplateBinding IsAccentColorsVisible}">
+            <!-- Left accent colors -->
+            <Grid Grid.Column="0"
+                  Height="{StaticResource ColorPreviewerAccentSectionHeight}"
+                  Width="{StaticResource ColorPreviewerAccentSectionWidth}"
+                  ColumnDefinitions="*,*"
+                  Margin="0,0,-10,0"
+                  VerticalAlignment="Center">
+              <Border Grid.Column="0"
+                      Grid.ColumnSpan="2"
+                      HorizontalAlignment="Stretch"
+                      VerticalAlignment="Stretch"
+                      Background="{StaticResource ColorControlCheckeredBackgroundBrush}" />
+              <Border x:Name="PART_AccentDecrement2Border"
+                      Grid.Column="0"
+                      CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}"
+                      Tag="-2"
+                      Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='-2'}" />
+              <Border x:Name="PART_AccentDecrement1Border"
+                      Grid.Column="1"
+                      Tag="-1"
+                      Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='-1'}" />
+            </Grid>
+            <!-- Right accent colors -->
+            <Grid Grid.Column="2"
+                  Height="{StaticResource ColorPreviewerAccentSectionHeight}"
+                  Width="{StaticResource ColorPreviewerAccentSectionWidth}"
+                  ColumnDefinitions="*,*"
+                  Margin="-10,0,0,0"
+                  VerticalAlignment="Center">
+              <Border Grid.Column="0"
+                      Grid.ColumnSpan="2"
+                      HorizontalAlignment="Stretch"
+                      VerticalAlignment="Stretch"
+                      Background="{StaticResource ColorControlCheckeredBackgroundBrush}" />
+              <Border x:Name="PART_AccentIncrement1Border"
+                      Grid.Column="0"
+                      Tag="1"
+                      Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='1'}" />
+              <Border x:Name="PART_AccentIncrement2Border"
+                      Grid.Column="1"
+                      CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource RightCornerRadiusFilterConverter}}"
+                      Tag="2"
+                      Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='2'}" />
+            </Grid>
+            <!-- Preview color: Must be last for drop shadow Z-index -->
+            <Border Grid.Column="1"
                     HorizontalAlignment="Stretch"
                     VerticalAlignment="Stretch"
-                    Background="{StaticResource CheckeredBackgroundBrush}" />
-            <Border x:Name="AccentInc1Border"
-                    Grid.Column="0"
-                    Tag="1"
-                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='1'}" />
-            <Border x:Name="AccentInc2Border"
-                    Grid.Column="1"
-                    CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource RightCornerRadiusFilterConverter}}"
-                    Tag="2"
-                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='2'}" />
+                    BoxShadow="0 0 10 2 #BF000000"
+                    CornerRadius="{TemplateBinding CornerRadius}"
+                    Margin="10">
+              <Panel>
+                <Border Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
+                        CornerRadius="{TemplateBinding CornerRadius}" />
+                <Border CornerRadius="{TemplateBinding CornerRadius}"
+                        Background="{TemplateBinding HsvColor, Converter={StaticResource ToBrushConverter}}"
+                        HorizontalAlignment="Stretch"
+                        VerticalAlignment="Stretch" />
+              </Panel>
+            </Border>
           </Grid>
-          <!-- Must be last for drop shadow Z-index -->
-          <Border Grid.Column="1"
-                  BoxShadow="0 0 10 2 #BF000000"
-                  CornerRadius="{TemplateBinding CornerRadius}"
-                  Margin="10">
+          <!-- Only preview color -->
+          <Border CornerRadius="{TemplateBinding CornerRadius}"
+                  IsVisible="{TemplateBinding IsAccentColorsVisible, Converter={x:Static BoolConverters.Not}}"
+                  HorizontalAlignment="Stretch"
+                  VerticalAlignment="Stretch"
+                  Margin="0,10,0,10">
             <Panel>
-              <Border Background="{StaticResource CheckeredBackgroundBrush}"
+              <Border Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
                       CornerRadius="{TemplateBinding CornerRadius}" />
-              <Border x:Name="PreviewBorder"
-                      CornerRadius="{TemplateBinding CornerRadius}"
-                      Background="{TemplateBinding HsvColor, Converter={StaticResource ToBrush}}"
+              <Border CornerRadius="{TemplateBinding CornerRadius}"
+                      Background="{TemplateBinding HsvColor, Converter={StaticResource ToBrushConverter}}"
                       HorizontalAlignment="Stretch"
                       VerticalAlignment="Stretch" />
             </Panel>
           </Border>
-        </Grid>
+        </Panel>
       </ControlTemplate>
     </Setter>
   </Style>

+ 12 - 18
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml

@@ -1,13 +1,7 @@
 <Styles xmlns="https://github.com/avaloniaui"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:converters="using:Avalonia.Controls.Converters"
         x:CompileBindings="True">
 
-  <Styles.Resources>
-    <converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadius" Corner="TopLeft" />
-    <converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadius" Corner="BottomRight" />
-  </Styles.Resources>
-
   <Style Selector="Thumb.ColorSliderThumbStyle">
     <Setter Property="BorderThickness" Value="0" />
     <Setter Property="Template">
@@ -27,21 +21,21 @@
     <Setter Property="CornerRadius" Value="10" />
     <Setter Property="Height" Value="20" />
     <Setter Property="Template">
-      <ControlTemplate>
+      <ControlTemplate TargetType="{x:Type ColorSlider}">
         <Border BorderThickness="{TemplateBinding BorderThickness}"
                 BorderBrush="{TemplateBinding BorderBrush}"
                 CornerRadius="{TemplateBinding CornerRadius}">
           <Grid Margin="{TemplateBinding Padding}">
             <Rectangle HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
-                       Fill="{StaticResource CheckeredBackgroundBrush}"
-                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
-                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+                       Fill="{StaticResource ColorControlCheckeredBackgroundBrush}"
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
             <Rectangle HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
                        Fill="{TemplateBinding Background}"
-                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
-                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
             <Track Name="PART_Track"
                    HorizontalAlignment="Stretch"
                    VerticalAlignment="Stretch"
@@ -99,21 +93,21 @@
     <Setter Property="CornerRadius" Value="10" />
     <Setter Property="Width" Value="20" />
     <Setter Property="Template">
-      <ControlTemplate>
+      <ControlTemplate TargetType="{x:Type ColorSlider}">
         <Border BorderThickness="{TemplateBinding BorderThickness}"
                 BorderBrush="{TemplateBinding BorderBrush}"
                 CornerRadius="{TemplateBinding CornerRadius}">
           <Grid Margin="{TemplateBinding Padding}">
             <Rectangle HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
-                       Fill="{StaticResource CheckeredBackgroundBrush}"
-                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
-                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+                       Fill="{StaticResource ColorControlCheckeredBackgroundBrush}"
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
             <Rectangle HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
                        Fill="{TemplateBinding Background}"
-                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
-                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
             <Track Name="PART_Track"
                    HorizontalAlignment="Stretch"
                    VerticalAlignment="Stretch"

+ 14 - 20
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml

@@ -1,18 +1,12 @@
 <Styles xmlns="https://github.com/avaloniaui"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:converters="using:Avalonia.Controls.Converters"
+        xmlns:controls="using:Avalonia.Controls"
         x:CompileBindings="True">
 
-  <Styles.Resources>
-    <converters:EnumValueEqualsConverter x:Key="EnumValueEquals" />
-    <converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadius" Corner="TopLeft" />
-    <converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadius" Corner="BottomRight" />
-  </Styles.Resources>
-
   <Style Selector="ColorSpectrum">
     <Setter Property="Template">
       <Setter.Value>
-        <ControlTemplate>
+        <ControlTemplate TargetType="{x:Type ColorSpectrum}">
           <Panel x:Name="PART_LayoutRoot"
                  HorizontalAlignment="Stretch"
                  VerticalAlignment="Stretch">
@@ -24,26 +18,26 @@
                          IsHitTestVisible="False"
                          HorizontalAlignment="Stretch"
                          VerticalAlignment="Stretch"
-                         IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}"
-                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
-                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+                         IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}"
+                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
               <Rectangle x:Name="PART_SpectrumOverlayRectangle"
                          IsHitTestVisible="False"
                          HorizontalAlignment="Stretch"
                          VerticalAlignment="Stretch"
-                         IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}"
-                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
-                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+                         IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}"
+                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
               <Ellipse x:Name="PART_SpectrumEllipse"
                        IsHitTestVisible="False"
                        HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
-                       IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Ring'}" />
+                       IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" />
               <Ellipse x:Name="PART_SpectrumOverlayEllipse"
                        IsHitTestVisible="False"
                        HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
-                       IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Ring'}" />
+                       IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" />
               <Canvas x:Name="PART_InputTarget"
                       Background="Transparent"
                       HorizontalAlignment="Stretch"
@@ -72,14 +66,14 @@
                          IsHitTestVisible="False"
                          HorizontalAlignment="Stretch"
                          VerticalAlignment="Stretch"
-                         IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}"
-                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
-                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+                         IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}"
+                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
               <Ellipse x:Name="BorderEllipse"
                        IsHitTestVisible="False"
                        HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
-                       IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Ring'}" />
+                       IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" />
             </Panel>
           </Panel>
         </ControlTemplate>

+ 14 - 2
src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml

@@ -1,8 +1,10 @@
 <Styles xmlns="https://github.com/avaloniaui"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:converters="using:Avalonia.Controls.Converters">
 
   <Styles.Resources>
-    <VisualBrush x:Key="CheckeredBackgroundBrush"
+    <!-- Shared Resources -->
+    <VisualBrush x:Key="ColorControlCheckeredBackgroundBrush"
                  TileMode="Tile"
                  Stretch="Uniform"
                  DestinationRect="0,0,8,8">
@@ -18,6 +20,16 @@
         </DrawingPresenter>
       </VisualBrush.Visual>
     </VisualBrush>
+    
+    <!-- Shared Converters -->
+    <converters:EnumToBoolConverter x:Key="EnumToBoolConverter" />
+    <converters:ToBrushConverter x:Key="ToBrushConverter" />
+    <converters:CornerRadiusFilterConverter x:Key="LeftCornerRadiusFilterConverter" Filter="TopLeft, BottomLeft"/>
+    <converters:CornerRadiusFilterConverter x:Key="RightCornerRadiusFilterConverter" Filter="TopRight, BottomRight"/>
+    <converters:CornerRadiusFilterConverter x:Key="TopCornerRadiusFilterConverter" Filter="TopLeft, TopRight"/>
+    <converters:CornerRadiusFilterConverter x:Key="BottomCornerRadiusFilterConverter" Filter="BottomLeft, BottomRight"/>
+    <converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadiusConverter" Corner="TopLeft" />
+    <converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadiusConverter" Corner="BottomRight" />
   </Styles.Resources>
 
   <!-- Primitives -->

+ 91 - 0
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml

@@ -0,0 +1,91 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:controls="using:Avalonia.Controls"
+        x:CompileBindings="True">
+
+  <Styles.Resources>
+    <!-- This must follow OverlayCornerRadius -->
+    <CornerRadius x:Key="TopOverlayCornerRadius">5,5,0,0</CornerRadius>
+  </Styles.Resources>
+
+  <Style Selector="ColorPicker">
+    <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
+    <Setter Property="Height" Value="32" />
+    <Setter Property="Width" Value="64" />
+    <Setter Property="MinWidth" Value="64" />
+    <Setter Property="Palette">
+      <controls:FluentColorPalette />
+    </Setter>
+    <Setter Property="Template">
+      <ControlTemplate TargetType="{x:Type ColorPicker}">
+        <DropDownButton CornerRadius="{TemplateBinding CornerRadius}"
+                        Height="{TemplateBinding Height}"
+                        Width="{TemplateBinding Width}"
+                        HorizontalContentAlignment="Stretch"
+                        VerticalContentAlignment="Stretch"
+                        Padding="0,0,10,0"
+                        UseLayoutRounding="False">
+          <DropDownButton.Styles>
+            <Style Selector="FlyoutPresenter.NoPadding">
+              <Setter Property="Padding" Value="0" />
+            </Style>
+          </DropDownButton.Styles>
+          <DropDownButton.Content>
+            <!-- Preview color -->
+            <Panel>
+              <Border Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
+                      CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}"
+                      HorizontalAlignment="Stretch"
+                      VerticalAlignment="Stretch"
+                      Margin="1,1,0,1" />
+              <Border Background="{TemplateBinding HsvColor, Converter={StaticResource ToBrushConverter}}"
+                      CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}"
+                      HorizontalAlignment="Stretch"
+                      VerticalAlignment="Stretch"
+                      Margin="1,1,0,1" />
+            </Panel>
+          </DropDownButton.Content>
+          <DropDownButton.Flyout>
+            <Flyout FlyoutPresenterClasses="NoPadding">
+              <ColorView x:Name="FlyoutColorView"
+                         Color="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
+                         ColorModel="{Binding ColorModel, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
+                         ColorSpectrumComponents="{TemplateBinding ColorSpectrumComponents}"
+                         ColorSpectrumShape="{TemplateBinding ColorSpectrumShape}"
+                         HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
+                         IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
+                         IsAlphaEnabled="{TemplateBinding IsAlphaEnabled}"
+                         IsAlphaVisible="{TemplateBinding IsAlphaVisible}"
+                         IsColorComponentsVisible="{TemplateBinding IsColorComponentsVisible}"
+                         IsColorModelVisible="{TemplateBinding IsColorModelVisible}"
+                         IsColorPaletteVisible="{TemplateBinding IsColorPaletteVisible}"
+                         IsColorPreviewVisible="{TemplateBinding IsColorPreviewVisible}"
+                         IsColorSpectrumVisible="{TemplateBinding IsColorSpectrumVisible}"
+                         IsColorSpectrumSliderVisible="{TemplateBinding IsColorSpectrumSliderVisible}"
+                         IsComponentSliderVisible="{TemplateBinding IsComponentSliderVisible}"
+                         IsComponentTextInputVisible="{TemplateBinding IsComponentTextInputVisible}"
+                         IsHexInputVisible="{TemplateBinding IsHexInputVisible}"
+                         MaxHue="{TemplateBinding MaxHue}"
+                         MaxSaturation="{TemplateBinding MaxSaturation}"
+                         MaxValue="{TemplateBinding MaxValue}"
+                         MinHue="{TemplateBinding MinHue}"
+                         MinSaturation="{TemplateBinding MinSaturation}"
+                         MinValue="{TemplateBinding MinValue}"
+                         PaletteColors="{TemplateBinding PaletteColors}"
+                         PaletteColumnCount="{TemplateBinding PaletteColumnCount}"
+                         Palette="{TemplateBinding Palette}"
+                         SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
+            </Flyout>
+          </DropDownButton.Flyout>
+        </DropDownButton>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+  <!-- Adjust Background within Flyout -->
+  <!-- Note: This is implemented but there seems to be an issue and the selector can't match across the Flyout -->
+  <Style Selector="ColorPicker /template/ ColorView#FlyoutColorView /template/ Border#TabBackgroundBorder">
+    <Setter Property="CornerRadius" Value="{DynamicResource TopOverlayCornerRadius}" />
+  </Style>
+
+</Styles>

+ 76 - 60
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml

@@ -1,84 +1,100 @@
 <Styles xmlns="https://github.com/avaloniaui"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:converters="using:Avalonia.Controls.Converters"
         xmlns:pc="using:Avalonia.Controls.Primitives.Converters"
         x:CompileBindings="True">
 
   <Styles.Resources>
-    <pc:AccentColorConverter x:Key="AccentColor" />
-    <converters:ToBrushConverter x:Key="ToBrush" />
-    <converters:CornerRadiusFilterConverter x:Key="RightCornerRadiusFilterConverter" Filter="TopRight, BottomRight"/>
-    <converters:CornerRadiusFilterConverter x:Key="LeftCornerRadiusFilterConverter" Filter="TopLeft, BottomLeft"/>
+    <pc:AccentColorConverter x:Key="AccentColorConverter" />
+    <x:Double x:Key="ColorPreviewerAccentSectionWidth">80</x:Double>
+    <x:Double x:Key="ColorPreviewerAccentSectionHeight">40</x:Double>
   </Styles.Resources>
 
   <Style Selector="ColorPreviewer">
     <Setter Property="Height" Value="70" />
     <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
     <Setter Property="Template">
-      <ControlTemplate>
-        <Grid ColumnDefinitions="Auto,*,Auto">
-          <!-- Left accent colors -->
-          <Grid Grid.Column="0"
-                Height="40"
-                Width="80"
-                ColumnDefinitions="*,*"
-                Margin="0,0,-10,0"
-                VerticalAlignment="Center"
-                IsVisible="{TemplateBinding ShowAccentColors}">
-            <Border Grid.Column="0"
-                    Grid.ColumnSpan="2"
-                    HorizontalAlignment="Stretch"
-                    VerticalAlignment="Stretch"
-                    Background="{StaticResource CheckeredBackgroundBrush}" />
-            <Border x:Name="AccentDec2Border"
-                    Grid.Column="0"
-                    CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}"
-                    Tag="-2"
-                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-2'}" />
-            <Border x:Name="AccentDec1Border"
-                    Grid.Column="1"
-                    Tag="-1"
-                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-1'}" />
-          </Grid>
-          <!-- Right accent colors -->
-          <Grid Grid.Column="2"
-                Height="40"
-                Width="80"
-                ColumnDefinitions="*,*"
-                Margin="-10,0,0,0"
-                VerticalAlignment="Center"
-                IsVisible="{TemplateBinding ShowAccentColors}">
-            <Border Grid.Column="0"
-                    Grid.ColumnSpan="2"
+      <ControlTemplate TargetType="{x:Type ColorPreviewer}">
+        <Panel>
+          <!-- Preview color with accents to the left and right -->
+          <Grid ColumnDefinitions="Auto,*,Auto"
+                IsVisible="{TemplateBinding IsAccentColorsVisible}">
+            <!-- Left accent colors -->
+            <Grid Grid.Column="0"
+                  Height="{StaticResource ColorPreviewerAccentSectionHeight}"
+                  Width="{StaticResource ColorPreviewerAccentSectionWidth}"
+                  ColumnDefinitions="*,*"
+                  Margin="0,0,-10,0"
+                  VerticalAlignment="Center">
+              <Border Grid.Column="0"
+                      Grid.ColumnSpan="2"
+                      HorizontalAlignment="Stretch"
+                      VerticalAlignment="Stretch"
+                      Background="{StaticResource ColorControlCheckeredBackgroundBrush}" />
+              <Border x:Name="PART_AccentDecrement2Border"
+                      Grid.Column="0"
+                      CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}"
+                      Tag="-2"
+                      Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='-2'}" />
+              <Border x:Name="PART_AccentDecrement1Border"
+                      Grid.Column="1"
+                      Tag="-1"
+                      Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='-1'}" />
+            </Grid>
+            <!-- Right accent colors -->
+            <Grid Grid.Column="2"
+                  Height="{StaticResource ColorPreviewerAccentSectionHeight}"
+                  Width="{StaticResource ColorPreviewerAccentSectionWidth}"
+                  ColumnDefinitions="*,*"
+                  Margin="-10,0,0,0"
+                  VerticalAlignment="Center">
+              <Border Grid.Column="0"
+                      Grid.ColumnSpan="2"
+                      HorizontalAlignment="Stretch"
+                      VerticalAlignment="Stretch"
+                      Background="{StaticResource ColorControlCheckeredBackgroundBrush}" />
+              <Border x:Name="PART_AccentIncrement1Border"
+                      Grid.Column="0"
+                      Tag="1"
+                      Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='1'}" />
+              <Border x:Name="PART_AccentIncrement2Border"
+                      Grid.Column="1"
+                      CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource RightCornerRadiusFilterConverter}}"
+                      Tag="2"
+                      Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColorConverter}, ConverterParameter='2'}" />
+            </Grid>
+            <!-- Preview color: Must be last for drop shadow Z-index -->
+            <Border Grid.Column="1"
                     HorizontalAlignment="Stretch"
                     VerticalAlignment="Stretch"
-                    Background="{StaticResource CheckeredBackgroundBrush}" />
-            <Border x:Name="AccentInc1Border"
-                    Grid.Column="0"
-                    Tag="1"
-                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='1'}" />
-            <Border x:Name="AccentInc2Border"
-                    Grid.Column="1"
-                    CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource RightCornerRadiusFilterConverter}}"
-                    Tag="2"
-                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='2'}" />
+                    BoxShadow="0 0 10 2 #BF000000"
+                    CornerRadius="{TemplateBinding CornerRadius}"
+                    Margin="10">
+              <Panel>
+                <Border Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
+                        CornerRadius="{TemplateBinding CornerRadius}" />
+                <Border CornerRadius="{TemplateBinding CornerRadius}"
+                        Background="{TemplateBinding HsvColor, Converter={StaticResource ToBrushConverter}}"
+                        HorizontalAlignment="Stretch"
+                        VerticalAlignment="Stretch" />
+              </Panel>
+            </Border>
           </Grid>
-          <!-- Must be last for drop shadow Z-index -->
-          <Border Grid.Column="1"
-                  BoxShadow="0 0 10 2 #BF000000"
-                  CornerRadius="{TemplateBinding CornerRadius}"
-                  Margin="10">
+          <!-- Only preview color -->
+          <Border CornerRadius="{TemplateBinding CornerRadius}"
+                  IsVisible="{TemplateBinding IsAccentColorsVisible, Converter={x:Static BoolConverters.Not}}"
+                  HorizontalAlignment="Stretch"
+                  VerticalAlignment="Stretch"
+                  Margin="0,10,0,10">
             <Panel>
-              <Border Background="{StaticResource CheckeredBackgroundBrush}"
+              <Border Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
                       CornerRadius="{TemplateBinding CornerRadius}" />
-              <Border x:Name="PreviewBorder"
-                      CornerRadius="{TemplateBinding CornerRadius}"
-                      Background="{TemplateBinding HsvColor, Converter={StaticResource ToBrush}}"
+              <Border CornerRadius="{TemplateBinding CornerRadius}"
+                      Background="{TemplateBinding HsvColor, Converter={StaticResource ToBrushConverter}}"
                       HorizontalAlignment="Stretch"
                       VerticalAlignment="Stretch" />
             </Panel>
           </Border>
-        </Grid>
+        </Panel>
       </ControlTemplate>
     </Setter>
   </Style>

+ 12 - 18
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml

@@ -1,13 +1,7 @@
 <Styles xmlns="https://github.com/avaloniaui"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:converters="using:Avalonia.Controls.Converters"
         x:CompileBindings="True">
 
-  <Styles.Resources>
-    <converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadius" Corner="TopLeft" />
-    <converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadius" Corner="BottomRight" />
-  </Styles.Resources>
-
   <Style Selector="Thumb.ColorSliderThumbStyle">
     <Setter Property="BorderThickness" Value="0" />
     <Setter Property="Template">
@@ -27,21 +21,21 @@
     <Setter Property="CornerRadius" Value="10" />
     <Setter Property="Height" Value="20" />
     <Setter Property="Template">
-      <ControlTemplate>
+      <ControlTemplate TargetType="{x:Type ColorSlider}">
         <Border BorderThickness="{TemplateBinding BorderThickness}"
                 BorderBrush="{TemplateBinding BorderBrush}"
                 CornerRadius="{TemplateBinding CornerRadius}">
           <Grid Margin="{TemplateBinding Padding}">
             <Rectangle HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
-                       Fill="{StaticResource CheckeredBackgroundBrush}"
-                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
-                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+                       Fill="{StaticResource ColorControlCheckeredBackgroundBrush}"
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
             <Rectangle HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
                        Fill="{TemplateBinding Background}"
-                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
-                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
             <Track Name="PART_Track"
                    HorizontalAlignment="Stretch"
                    VerticalAlignment="Stretch"
@@ -99,21 +93,21 @@
     <Setter Property="CornerRadius" Value="10" />
     <Setter Property="Width" Value="20" />
     <Setter Property="Template">
-      <ControlTemplate>
+      <ControlTemplate TargetType="{x:Type ColorSlider}">
         <Border BorderThickness="{TemplateBinding BorderThickness}"
                 BorderBrush="{TemplateBinding BorderBrush}"
                 CornerRadius="{TemplateBinding CornerRadius}">
           <Grid Margin="{TemplateBinding Padding}">
             <Rectangle HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
-                       Fill="{StaticResource CheckeredBackgroundBrush}"
-                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
-                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+                       Fill="{StaticResource ColorControlCheckeredBackgroundBrush}"
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
             <Rectangle HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
                        Fill="{TemplateBinding Background}"
-                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
-                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
             <Track Name="PART_Track"
                    HorizontalAlignment="Stretch"
                    VerticalAlignment="Stretch"

+ 14 - 20
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml

@@ -1,18 +1,12 @@
 <Styles xmlns="https://github.com/avaloniaui"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:converters="using:Avalonia.Controls.Converters"
+        xmlns:controls="using:Avalonia.Controls"
         x:CompileBindings="True">
 
-  <Styles.Resources>
-    <converters:EnumValueEqualsConverter x:Key="EnumValueEquals" />
-    <converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadius" Corner="TopLeft" />
-    <converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadius" Corner="BottomRight" />
-  </Styles.Resources>
-
   <Style Selector="ColorSpectrum">
     <Setter Property="Template">
       <Setter.Value>
-        <ControlTemplate>
+        <ControlTemplate TargetType="{x:Type ColorSpectrum}">
           <Panel x:Name="PART_LayoutRoot"
                  HorizontalAlignment="Stretch"
                  VerticalAlignment="Stretch">
@@ -24,26 +18,26 @@
                          IsHitTestVisible="False"
                          HorizontalAlignment="Stretch"
                          VerticalAlignment="Stretch"
-                         IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}"
-                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
-                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+                         IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}"
+                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
               <Rectangle x:Name="PART_SpectrumOverlayRectangle"
                          IsHitTestVisible="False"
                          HorizontalAlignment="Stretch"
                          VerticalAlignment="Stretch"
-                         IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}"
-                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
-                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+                         IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}"
+                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
               <Ellipse x:Name="PART_SpectrumEllipse"
                        IsHitTestVisible="False"
                        HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
-                       IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Ring'}" />
+                       IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" />
               <Ellipse x:Name="PART_SpectrumOverlayEllipse"
                        IsHitTestVisible="False"
                        HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
-                       IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Ring'}" />
+                       IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" />
               <Canvas x:Name="PART_InputTarget"
                       Background="Transparent"
                       HorizontalAlignment="Stretch"
@@ -72,14 +66,14 @@
                          IsHitTestVisible="False"
                          HorizontalAlignment="Stretch"
                          VerticalAlignment="Stretch"
-                         IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}"
-                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
-                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+                         IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorSpectrumShape.Box}}"
+                         RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}"
+                         RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" />
               <Ellipse x:Name="BorderEllipse"
                        IsHitTestVisible="False"
                        HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch"
-                       IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Ring'}" />
+                       IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorSpectrumShape.Ring}}" />
             </Panel>
           </Panel>
         </ControlTemplate>

+ 650 - 0
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@@ -0,0 +1,650 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:controls="using:Avalonia.Controls"
+        xmlns:converters="using:Avalonia.Controls.Converters"
+        xmlns:primitives="using:Avalonia.Controls.Primitives"
+        xmlns:pc="clr-namespace:Avalonia.Controls.Primitives.Converters;assembly=Avalonia.Controls.ColorPicker"
+        xmlns:globalization="clr-namespace:System.Globalization;assembly=mscorlib"
+        x:CompileBindings="True">
+
+  <Styles.Resources>
+    <pc:ContrastBrushConverter x:Key="ContrastBrushConverter" />
+    <pc:ThirdComponentConverter x:Key="ThirdComponentConverter" />
+    <converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
+    <converters:ColorToHexConverter x:Key="ColorToHexConverter" />
+    <globalization:NumberFormatInfo x:Key="ColorViewComponentNumberFormat" NumberDecimalDigits="0" />
+    <x:Double x:Key="ColorViewTabStripHeight">48</x:Double>
+    <x:Double x:Key="ColorViewComponentLabelWidth">30</x:Double>
+    <x:Double x:Key="ColorViewComponentTextInputWidth">80</x:Double>
+    <!-- Fluent UI System Icons : ic_fluent_inking_tool_20_regular.svg -->
+    <PathGeometry x:Key="ColorViewSpectrumIconGeometry">
+      M3 2C3.27614 2 3.5 2.22386 3.5 2.5V5.5C3.5 5.77614 3.72386 6 4 6H16C16.2761 6 16.5 5.77614
+      16.5 5.5V2.5C16.5 2.22386 16.7239 2 17 2C17.2761 2 17.5 2.22386 17.5 2.5V5.5C17.5 6.32843
+      16.8284 7 16 7H15.809L12.2236 14.1708C12.0615 14.4951 11.7914 14.7431 11.4695
+      14.8802C11.4905 15.0808 11.5 15.2891 11.5 15.5C11.5 16.0818 11.4278 16.6623 11.2268
+      17.1165C11.019 17.5862 10.6266 18 10 18C9.37343 18 8.98105 17.5862 8.77323 17.1165C8.57222
+      16.6623 8.5 16.0818 8.5 15.5C8.5 15.2891 8.50952 15.0808 8.53051 14.8802C8.20863 14.7431
+      7.93851 14.4951 7.77639 14.1708L4.19098 7H4C3.17157 7 2.5 6.32843 2.5 5.5V2.5C2.5 2.22386
+      2.72386 2 3 2ZM9.11803 14H10.882C11.0714 14 11.2445 13.893 11.3292 13.7236L14.691
+      7H5.30902L8.67082 13.7236C8.75552 13.893 8.92865 14 9.11803 14ZM9.52346 15C9.50787 15.1549
+      9.5 15.3225 9.5 15.5C9.5 16.0228 9.56841 16.4423 9.6877 16.7119C9.8002 16.9661 9.90782 17
+      10 17C10.0922 17 10.1998 16.9661 10.3123 16.7119C10.4316 16.4423 10.5 16.0228 10.5
+      15.5C10.5 15.3225 10.4921 15.1549 10.4765 15H9.52346Z
+    </PathGeometry>
+    <!-- Fluent UI System Icons : ic_fluent_color_20_regular.svg -->
+    <PathGeometry x:Key="ColorViewPaletteIconGeometry">
+      M9.75003 6.5C10.1642 6.5 10.5 6.16421 10.5 5.75C10.5 5.33579 10.1642 5 9.75003 5C9.33582
+      5 9.00003 5.33579 9.00003 5.75C9.00003 6.16421 9.33582 6.5 9.75003 6.5ZM12.75 7.5C13.1642
+      7.5 13.5 7.16421 13.5 6.75C13.5 6.33579 13.1642 6 12.75 6C12.3358 6 12 6.33579 12 6.75C12
+      7.16421 12.3358 7.5 12.75 7.5ZM15.25 9C15.25 9.41421 14.9142 9.75 14.5 9.75C14.0858 9.75
+      13.75 9.41421 13.75 9C13.75 8.58579 14.0858 8.25 14.5 8.25C14.9142 8.25 15.25 8.58579
+      15.25 9ZM14.5 12.75C14.9142 12.75 15.25 12.4142 15.25 12C15.25 11.5858 14.9142 11.25 14.5
+      11.25C14.0858 11.25 13.75 11.5858 13.75 12C13.75 12.4142 14.0858 12.75 14.5 12.75ZM13.25
+      14C13.25 14.4142 12.9142 14.75 12.5 14.75C12.0858 14.75 11.75 14.4142 11.75 14C11.75
+      13.5858 12.0858 13.25 12.5 13.25C12.9142 13.25 13.25 13.5858 13.25 14ZM13.6972
+      2.99169C10.9426 1.57663 8.1432 1.7124 5.77007 3.16636C4.55909 3.9083 3.25331 5.46925
+      2.51605 7.05899C2.14542 7.85816 1.89915 8.70492 1.90238 9.49318C1.90566 10.2941 2.16983
+      11.0587 2.84039 11.6053C3.45058 12.1026 3.98165 12.353 4.49574 12.3784C5.01375 12.404
+      5.41804 12.1942 5.73429 12.0076C5.80382 11.9666 5.86891 11.927 5.93113 11.8892C6.17332
+      11.7421 6.37205 11.6214 6.62049 11.5426C6.90191 11.4534 7.2582 11.4205 7.77579
+      11.5787C7.96661 11.637 8.08161 11.7235 8.16212 11.8229C8.24792 11.9289 8.31662 12.0774
+      8.36788 12.2886C8.41955 12.5016 8.44767 12.7527 8.46868 13.0491C8.47651 13.1594 8.48379
+      13.2855 8.49142 13.4176C8.50252 13.6098 8.51437 13.8149 8.52974 14.0037C8.58435 14.6744
+      8.69971 15.4401 9.10362 16.1357C9.51764 16.8488 10.2047 17.439 11.307 17.8158C12.9093
+      18.3636 14.3731 17.9191 15.5126 17.0169C16.6391 16.125 17.4691 14.7761 17.8842
+      13.4272C19.1991 9.15377 17.6728 5.03394 13.6972 2.99169ZM6.29249 4.01905C8.35686 2.75426
+      10.7844 2.61959 13.2403 3.88119C16.7473 5.68275 18.1135 9.28161 16.9284 13.1332C16.5624
+      14.3227 15.8338 15.4871 14.8919 16.2329C13.963 16.9684 12.8486 17.286 11.6305
+      16.8696C10.7269 16.5607 10.2467 16.1129 9.96842 15.6336C9.68001 15.1369 9.57799 14.5556
+      9.52644 13.9225C9.51101 13.733 9.50132 13.5621 9.49147 13.3884C9.48399 13.2564 9.47642
+      13.1229 9.46618 12.9783C9.44424 12.669 9.41175 12.3499 9.33968 12.0529C9.26719 11.7541
+      9.14902 11.4527 8.93935 11.1937C8.72439 10.9282 8.43532 10.7346 8.06801 10.6223C7.36648
+      10.408 6.80266 10.4359 6.31839 10.5893C5.94331 10.7082 5.62016 10.9061 5.37179
+      11.0582C5.31992 11.0899 5.2713 11.1197 5.22616 11.1463C4.94094 11.3146 4.75357 11.39
+      4.54514 11.3796C4.33279 11.3691 4.00262 11.2625 3.47218 10.8301C3.0866 10.5158 2.90473
+      10.0668 2.90237 9.48908C2.89995 8.89865 3.08843 8.20165 3.42324 7.47971C4.09686 6.0272
+      5.28471 4.63649 6.29249 4.01905Z
+    </PathGeometry>
+    <!-- Fluent UI System Icons : ic_fluent_options_20_regular.svg -->
+    <PathGeometry x:Key="ColorViewComponentsIconGeometry">
+      M14.95 5C14.7184 3.85888 13.7095 3 12.5 3C11.2905 3 10.2816 3.85888 10.05 5H2.5C2.22386
+      5 2 5.22386 2 5.5C2 5.77614 2.22386 6 2.5 6H10.05C10.2816 7.14112 11.2905 8 12.5 8C13.7297
+      8 14.752 7.11217 14.961 5.94254C14.9575 5.96177 14.9539 5.98093 14.95 6H17.5C17.7761 6 18
+      5.77614 18 5.5C18 5.22386 17.7761 5 17.5 5H14.95ZM12.5 7C11.6716 7 11 6.32843 11 5.5C11
+      4.67157 11.6716 4 12.5 4C13.3284 4 14 4.67157 14 5.5C14 6.32843 13.3284 7 12.5 7ZM9.94999
+      14C9.71836 12.8589 8.70948 12 7.5 12C6.29052 12 5.28164 12.8589 5.05001 14H2.5C2.22386
+      14 2 14.2239 2 14.5C2 14.7761 2.22386 15 2.5 15H5.05001C5.28164 16.1411 6.29052 17 7.5
+      17C8.70948 17 9.71836 16.1411 9.94999 15H17.5C17.7761 15 18 14.7761 18 14.5C18 14.2239
+      17.7761 14 17.5 14H9.94999ZM7.5 16C6.67157 16 6 15.3284 6 14.5C6 13.6716 6.67157 13 7.5
+      13C8.32843 13 9 13.6716 9 14.5C9 15.3284 8.32843 16 7.5 16Z
+    </PathGeometry>
+  </Styles.Resources>
+
+  <Style Selector="ColorView">
+    <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
+    <Setter Property="Palette">
+      <controls:FluentColorPalette />
+    </Setter>
+    <Setter Property="Template">
+      <ControlTemplate TargetType="{x:Type ColorView}">
+        <Grid RowDefinitions="Auto,Auto">
+
+          <!-- Backgrounds -->
+          <!-- These are separated for Fluent v2 re-styling without having to re-template. -->
+          <Border x:Name="TabBackgroundBorder"
+                  Grid.Row="0"
+                  Grid.RowSpan="2"
+                  Height="48"
+                  HorizontalAlignment="Stretch"
+                  VerticalAlignment="Top"
+                  Background="{DynamicResource SystemControlBackgroundBaseLowBrush}"
+                  CornerRadius="{TemplateBinding CornerRadius}" />
+          <Border x:Name="ContentBackgroundBorder"
+                  Grid.Row="0"
+                  Grid.RowSpan="2"
+                  Margin="0,48,0,0"
+                  HorizontalAlignment="Stretch"
+                  VerticalAlignment="Stretch"
+                  CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource BottomCornerRadiusFilterConverter}}"
+                  Background="Transparent"
+                  BorderBrush="Transparent"
+                  BorderThickness="0,1,0,0" />
+
+          <TabControl x:Name="PART_TabControl"
+                      Grid.Row="0"
+                      Height="338"
+                      Width="350"
+                      SelectedIndex="{Binding SelectedIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
+            <TabControl.Resources>
+              <Thickness x:Key="TabItemMargin">0,0,0,0</Thickness>
+            </TabControl.Resources>
+            <TabControl.Styles>
+              <Style Selector="TabControl">
+                <Setter Property="ItemsPanel">
+                  <Setter.Value>
+                    <ItemsPanelTemplate>
+                      <UniformGrid Columns="0"
+                                   Rows="1" />
+                    </ItemsPanelTemplate>
+                  </Setter.Value>
+                </Setter>
+              </Style>
+            </TabControl.Styles>
+            <!-- Spectrum Tab -->
+            <TabItem IsVisible="{TemplateBinding IsColorSpectrumVisible}">
+              <TabItem.Header>
+                <Border Height="{DynamicResource ColorViewTabStripHeight}">
+                  <PathIcon Width="20"
+                            Height="20"
+                            Data="{DynamicResource ColorViewSpectrumIconGeometry}" />
+                </Border>
+              </TabItem.Header>
+              <Grid RowDefinitions="*"
+                    Margin="12">
+                <Grid.ColumnDefinitions>
+                  <ColumnDefinition Width="Auto"
+                                    MinWidth="32" />
+                  <ColumnDefinition Width="*" />
+                  <ColumnDefinition Width="Auto"
+                                    MinWidth="32" />
+                </Grid.ColumnDefinitions>
+                <primitives:ColorSlider x:Name="ColorSpectrumThirdComponentSlider"
+                                        AutomationProperties.Name="Third Component"
+                                        Grid.Column="0"
+                                        IsAlphaMaxForced="True"
+                                        IsSaturationValueMaxForced="False"
+                                        Orientation="Vertical"
+                                        ColorModel="Hsva"
+                                        ColorComponent="{Binding Components, ElementName=ColorSpectrum, Converter={StaticResource ThirdComponentConverter}}"
+                                        HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}"
+                                        HorizontalAlignment="Center"
+                                        VerticalAlignment="Stretch"
+                                        Margin="0,0,12,0"
+                                        IsVisible="{TemplateBinding IsColorSpectrumSliderVisible}"/>
+                <primitives:ColorSpectrum x:Name="ColorSpectrum"
+                                          Grid.Column="1"
+                                          Components="{TemplateBinding ColorSpectrumComponents}"
+                                          HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
+                                          MinHue="{TemplateBinding MinHue}"
+                                          MaxHue="{TemplateBinding MaxHue}"
+                                          MinSaturation="{TemplateBinding MinSaturation}"
+                                          MaxSaturation="{TemplateBinding MaxSaturation}"
+                                          MinValue="{TemplateBinding MinValue}"
+                                          MaxValue="{TemplateBinding MaxValue}"
+                                          Shape="{TemplateBinding ColorSpectrumShape}"
+                                          HorizontalAlignment="Stretch"
+                                          VerticalAlignment="Stretch" />
+                <primitives:ColorSlider x:Name="ColorSpectrumAlphaSlider"
+                                        AutomationProperties.Name="Alpha Component"
+                                        Grid.Column="2"
+                                        Orientation="Vertical"
+                                        ColorModel="Hsva"
+                                        ColorComponent="Alpha"
+                                        HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}"
+                                        HorizontalAlignment="Center"
+                                        VerticalAlignment="Stretch"
+                                        Margin="12,0,0,0"
+                                        IsEnabled="{TemplateBinding IsAlphaEnabled}">
+                  <primitives:ColorSlider.IsVisible>
+                    <MultiBinding Converter="{x:Static BoolConverters.And}">
+                      <Binding RelativeSource="{RelativeSource TemplatedParent}"
+                               Path="IsAlphaVisible" />
+                    </MultiBinding>
+                  </primitives:ColorSlider.IsVisible>
+                </primitives:ColorSlider>
+              </Grid>
+            </TabItem>
+            <!-- Palette Tab -->
+            <TabItem IsVisible="{TemplateBinding IsColorPaletteVisible}">
+              <TabItem.Header>
+                <Border Height="{DynamicResource ColorViewTabStripHeight}">
+                  <PathIcon Width="20"
+                            Height="20"
+                            Data="{DynamicResource ColorViewPaletteIconGeometry}" />
+                </Border>
+              </TabItem.Header>
+              <ListBox Items="{TemplateBinding PaletteColors}"
+                       SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
+                       UseLayoutRounding="False"
+                       Margin="12">
+                <ListBox.Styles>
+
+                  <Style Selector="ListBoxItem">
+                    <Setter Property="Background" Value="Transparent" />
+                    <Setter Property="Padding" Value="0" />
+                    <Setter Property="Template">
+                      <ControlTemplate TargetType="{x:Type ListBoxItem}">
+                        <Grid UseLayoutRounding="False">
+                          <ContentPresenter Name="PART_ContentPresenter"
+                                            Background="{TemplateBinding Background}"
+                                            BorderBrush="{TemplateBinding BorderBrush}"
+                                            BorderThickness="{TemplateBinding BorderThickness}"
+                                            CornerRadius="{TemplateBinding CornerRadius}"
+                                            ContentTemplate="{TemplateBinding ContentTemplate}"
+                                            Content="{TemplateBinding Content}"
+                                            Padding="{TemplateBinding Padding}"
+                                            VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
+                                            HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" />
+                          <Rectangle x:Name="BorderRectangle"
+                                     IsHitTestVisible="False"
+                                     StrokeThickness="3"
+                                     HorizontalAlignment="Stretch"
+                                     VerticalAlignment="Stretch" />
+                        </Grid>
+                      </ControlTemplate>
+                    </Setter>
+
+                    <Style Selector="^ /template/ Rectangle#BorderRectangle">
+                      <Setter Property="Stroke" Value="{DynamicResource SystemControlHighlightListAccentLowBrush}" />
+                      <Setter Property="Opacity" Value="0" />
+                    </Style>
+
+                    <Style Selector="^:pointerover /template/ Rectangle#BorderRectangle">
+                      <Setter Property="Stroke" Value="{DynamicResource SystemControlHighlightListAccentLowBrush}" />
+                      <Setter Property="Opacity" Value="1" />
+                    </Style>
+
+                    <Style Selector="^:selected /template/ Rectangle#BorderRectangle"
+                           x:DataType="Color">
+                      <!-- The below line really should be:
+                             'Value="{Binding Converter={StaticResource ContrastBrushConverter}, ConverterParameter={DynamicResource TextControlForeground}}"'
+                           However, DynamicResource and Binding is not currently supported in Avalonia for converter parameters.
+                           This means certain colors with transparency less than 50% may have a selection border that is difficult
+                           to see over top of the default control/window background. Since palettes do not usually have transparency
+                           it is considered better to disable this functionality rather than work around it with a multi-value
+                           converter. This should be revisited if Avalonia supports the above code in the future. -->
+                      <Setter Property="Stroke" Value="{Binding Converter={StaticResource ContrastBrushConverter}}" />
+                      <Setter Property="Opacity" Value="1" />
+                    </Style>
+                  </Style>
+
+                </ListBox.Styles>
+                <ListBox.ItemTemplate>
+                  <DataTemplate DataType="{x:Type Color}">
+                    <Border AutomationProperties.Name="{Binding Converter={StaticResource ColorToDisplayNameConverter}}"
+                            ToolTip.Tip="{Binding Converter={StaticResource ColorToDisplayNameConverter}}"
+                            HorizontalAlignment="Stretch"
+                            VerticalAlignment="Stretch">
+                      <Border.Background>
+                        <SolidColorBrush Color="{Binding}" />
+                      </Border.Background>
+                    </Border>
+                  </DataTemplate>
+                </ListBox.ItemTemplate>
+                <ListBox.ItemsPanel>
+                  <ItemsPanelTemplate>
+                    <UniformGrid Columns="{Binding $parent[ColorView].PaletteColumnCount}" />
+                  </ItemsPanelTemplate>
+                </ListBox.ItemsPanel>
+              </ListBox>
+            </TabItem>
+            <!-- Components Tab -->
+            <TabItem IsVisible="{TemplateBinding IsColorComponentsVisible}">
+              <TabItem.Header>
+                <Border Height="{DynamicResource ColorViewTabStripHeight}">
+                  <PathIcon Width="20"
+                            Height="20"
+                            Data="{DynamicResource ColorViewComponentsIconGeometry}" />
+                </Border>
+              </TabItem.Header>
+              <Grid ColumnDefinitions="Auto,Auto,*"
+                    RowDefinitions="Auto,24,1*,1*,1*,1*,12"
+                    Margin="12">
+                <!-- Top color model & Hex input -->
+                <Grid Grid.Column="0"
+                      Grid.ColumnSpan="3"
+                      Grid.Row="0"
+                      ColumnDefinitions="1*,12,1*">
+                  <!-- Content RGB/HSV names are hard-coded and considered universal -->
+                  <!-- RadioButtons are styled to look like a 'SegmentedControl' or 'ButtonGroup' -->
+                  <Grid ColumnDefinitions="1*,1*"
+                        IsVisible="{TemplateBinding IsColorModelVisible}">
+                    <Grid.Styles>
+
+                      <Style Selector="RadioButton">
+                        <Setter Property="Background" Value="{DynamicResource ToggleButtonBackground}" />
+                        <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForeground}" />
+                        <Setter Property="BorderBrush" Value="{DynamicResource SystemControlHighlightAccentBrush}" />
+                        <Setter Property="BorderThickness" Value="1" />
+                        <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
+                        <Setter Property="Padding" Value="{DynamicResource ButtonPadding}" />
+                        <Setter Property="HorizontalAlignment" Value="Stretch" />
+                        <Setter Property="VerticalAlignment" Value="Center" />
+                        <Setter Property="HorizontalContentAlignment" Value="Center" />
+                        <Setter Property="VerticalContentAlignment" Value="Center" />
+                        <Setter Property="FontWeight" Value="Normal" />
+                        <Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
+                        <Setter Property="Template">
+                          <ControlTemplate TargetType="{x:Type RadioButton}">
+                            <ContentPresenter x:Name="PART_ContentPresenter"
+                                              Background="{TemplateBinding Background}"
+                                              BorderBrush="{TemplateBinding BorderBrush}"
+                                              BorderThickness="{TemplateBinding BorderThickness}"
+                                              CornerRadius="{TemplateBinding CornerRadius}"
+                                              Content="{TemplateBinding Content}"
+                                              ContentTemplate="{TemplateBinding ContentTemplate}"
+                                              Padding="{TemplateBinding Padding}"
+                                              RecognizesAccessKey="True"
+                                              HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
+                                              VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
+                          </ControlTemplate>
+                        </Setter>
+
+                        <Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
+                          <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
+                        </Style>
+
+                        <Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
+                          <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundPointerOver}" />
+                          <Setter Property="BorderBrush" Value="{DynamicResource SystemControlHighlightAccentBrush}" />
+                          <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundPointerOver}" />
+                        </Style>
+
+                        <Style Selector="^:pressed  /template/ ContentPresenter#PART_ContentPresenter">
+                          <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundPressed}" />
+                          <Setter Property="BorderBrush" Value="{DynamicResource SystemControlHighlightAccentBrush}" />
+                          <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundPressed}" />
+                        </Style>
+
+                        <Style Selector="^:checked /template/ ContentPresenter#PART_ContentPresenter">
+                          <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundChecked}" />
+                          <Setter Property="BorderBrush" Value="{DynamicResource SystemControlHighlightAccentBrush}" />
+                          <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundChecked}" />
+                        </Style>
+
+                        <Style Selector="^:checked:pointerover /template/ ContentPresenter#PART_ContentPresenter">
+                          <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundCheckedPointerOver}" />
+                          <Setter Property="BorderBrush" Value="{DynamicResource SystemControlHighlightAccentBrush}" />
+                          <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundCheckedPointerOver}" />
+                        </Style>
+
+                        <Style Selector="^:checked:pressed /template/ ContentPresenter#PART_ContentPresenter">
+                          <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundCheckedPressed}" />
+                          <Setter Property="BorderBrush" Value="{DynamicResource SystemControlHighlightAccentBrush}" />
+                          <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundCheckedPressed}" />
+                        </Style>
+                      </Style>
+
+                    </Grid.Styles>
+                    <RadioButton x:Name="RgbRadioButton"
+                                 Grid.Column="0"
+                                 Content="RGB"
+                                 CornerRadius="4,0,0,4"
+                                 IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" />
+                    <RadioButton x:Name="HsvRadioButton"
+                                 Grid.Column="1"
+                                 Content="HSV"
+                                 CornerRadius="0,4,4,0"
+                                 IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" />
+                  </Grid>
+                  <Grid x:Name="HexInputGrid"
+                        Grid.Column="2"
+                        IsVisible="{TemplateBinding IsHexInputVisible}">
+                    <Grid.ColumnDefinitions>
+                      <ColumnDefinition Width="30" />
+                      <ColumnDefinition Width="*" />
+                    </Grid.ColumnDefinitions>
+                    <Border Grid.Column="0"
+                            Height="32"
+                            Background="{DynamicResource TextControlBackgroundDisabled}"
+                            BorderBrush="{DynamicResource TextControlBorderBrush}"
+                            BorderThickness="1,1,0,1"
+                            CornerRadius="4,0,0,4">
+                      <TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
+                                 FontWeight="SemiBold"
+                                 Text="#"
+                                 HorizontalAlignment="Center"
+                                 VerticalAlignment="Center" />
+                    </Border>
+                    <!-- Color updated in code-behind -->
+                    <TextBox x:Name="PART_HexTextBox"
+                             Grid.Column="1"
+                             AutomationProperties.Name="Hexadecimal Color"
+                             Height="32"
+                             MaxLength="8"
+                             HorizontalAlignment="Stretch"
+                             CornerRadius="0,4,4,0" />
+                  </Grid>
+                </Grid>
+                <!-- Color component editing controls -->
+                <!-- Component 1 RGB:Red HSV:Hue -->
+                <Border Grid.Column="0"
+                        Grid.Row="2"
+                        Height="{Binding ElementName=Component1NumericUpDown, Path=Bounds.Height}"
+                        Width="{DynamicResource ColorViewComponentLabelWidth}"
+                        Background="{DynamicResource TextControlBackgroundDisabled}"
+                        BorderBrush="{DynamicResource TextControlBorderBrush}"
+                        BorderThickness="1,1,0,1"
+                        CornerRadius="4,0,0,4"
+                        VerticalAlignment="Center"
+                        IsVisible="{TemplateBinding IsComponentTextInputVisible}">
+                  <Panel HorizontalAlignment="Center"
+                         VerticalAlignment="Center">
+                    <TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
+                               FontWeight="SemiBold"
+                               Text="R"
+                               IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}"/>
+                    <TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
+                               FontWeight="SemiBold"
+                               Text="H"
+                               IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" />
+                  </Panel>
+                </Border>
+                <NumericUpDown x:Name="Component1NumericUpDown"
+                               Grid.Column="1"
+                               Grid.Row="2"
+                               AllowSpin="True"
+                               ShowButtonSpinner="False"
+                               Height="32"
+                               Width="{DynamicResource ColorViewComponentTextInputWidth}"
+                               CornerRadius="0,4,4,0"
+                               Margin="0,0,12,0"
+                               VerticalAlignment="Center"
+                               NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
+                               Minimum="{Binding Minimum, ElementName=Component1Slider}"
+                               Maximum="{Binding Maximum, ElementName=Component1Slider}"
+                               Value="{Binding Value, ElementName=Component1Slider}"
+                               IsVisible="{TemplateBinding IsComponentTextInputVisible}" />
+                <primitives:ColorSlider x:Name="Component1Slider"
+                                        Grid.Column="2"
+                                        Grid.Row="2"
+                                        Orientation="Horizontal"
+                                        IsRoundingEnabled="True"
+                                        IsSnapToTickEnabled="True"
+                                        TickFrequency="1"
+                                        ColorComponent="Component1"
+                                        ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
+                                        HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
+                                        HorizontalAlignment="Stretch"
+                                        VerticalAlignment="Center"
+                                        IsVisible="{TemplateBinding IsComponentSliderVisible}" />
+                <!-- Component 2 RGB:Green HSV:Saturation -->
+                <Border Grid.Column="0"
+                        Grid.Row="3"
+                        Width="{DynamicResource ColorViewComponentLabelWidth}"
+                        Height="{Binding ElementName=Component2NumericUpDown, Path=Bounds.Height}"
+                        Background="{DynamicResource TextControlBackgroundDisabled}"
+                        BorderBrush="{DynamicResource TextControlBorderBrush}"
+                        BorderThickness="1,1,0,1"
+                        CornerRadius="4,0,0,4"
+                        VerticalAlignment="Center"
+                        IsVisible="{TemplateBinding IsComponentTextInputVisible}">
+                  <Panel HorizontalAlignment="Center"
+                         VerticalAlignment="Center">
+                    <TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
+                               FontWeight="SemiBold"
+                               Text="G"
+                               IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" />
+                    <TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
+                               FontWeight="SemiBold"
+                               Text="S"
+                               IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" />
+                  </Panel>
+                </Border>
+                <NumericUpDown x:Name="Component2NumericUpDown"
+                               Grid.Column="1"
+                               Grid.Row="3"
+                               AllowSpin="True"
+                               ShowButtonSpinner="False"
+                               Height="32"
+                               Width="{DynamicResource ColorViewComponentTextInputWidth}"
+                               CornerRadius="0,4,4,0"
+                               Margin="0,0,12,0"
+                               VerticalAlignment="Center"
+                               NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
+                               Minimum="{Binding Minimum, ElementName=Component2Slider}"
+                               Maximum="{Binding Maximum, ElementName=Component2Slider}"
+                               Value="{Binding Value, ElementName=Component2Slider}"
+                               IsVisible="{TemplateBinding IsComponentTextInputVisible}" />
+                <primitives:ColorSlider x:Name="Component2Slider"
+                                        Grid.Column="2"
+                                        Grid.Row="3"
+                                        Orientation="Horizontal"
+                                        IsRoundingEnabled="True"
+                                        IsSnapToTickEnabled="True"
+                                        TickFrequency="1"
+                                        ColorComponent="Component2"
+                                        ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
+                                        HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
+                                        HorizontalAlignment="Stretch"
+                                        VerticalAlignment="Center"
+                                        IsVisible="{TemplateBinding IsComponentSliderVisible}" />
+                <!-- Component 3 RGB:Blue HSV:Value -->
+                <Border Grid.Column="0"
+                        Grid.Row="4"
+                        Width="{DynamicResource ColorViewComponentLabelWidth}"
+                        Height="{Binding ElementName=Component3NumericUpDown, Path=Bounds.Height}"
+                        Background="{DynamicResource TextControlBackgroundDisabled}"
+                        BorderBrush="{DynamicResource TextControlBorderBrush}"
+                        BorderThickness="1,1,0,1"
+                        CornerRadius="4,0,0,4"
+                        VerticalAlignment="Center"
+                        IsVisible="{TemplateBinding IsComponentTextInputVisible}">
+                  <Panel HorizontalAlignment="Center"
+                         VerticalAlignment="Center">
+                    <TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
+                               FontWeight="SemiBold"
+                               Text="B"
+                               IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=OneWay}" />
+                    <TextBlock Foreground="{DynamicResource TextControlForegroundDisabled}"
+                               FontWeight="SemiBold"
+                               Text="V"
+                               IsVisible="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=OneWay}" />
+                  </Panel>
+                </Border>
+                <NumericUpDown x:Name="Component3NumericUpDown"
+                               Grid.Column="1"
+                               Grid.Row="4"
+                               AllowSpin="True"
+                               ShowButtonSpinner="False"
+                               Height="32"
+                               Width="{DynamicResource ColorViewComponentTextInputWidth}"
+                               CornerRadius="0,4,4,0"
+                               Margin="0,0,12,0"
+                               VerticalAlignment="Center"
+                               NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
+                               Minimum="{Binding Minimum, ElementName=Component3Slider}"
+                               Maximum="{Binding Maximum, ElementName=Component3Slider}"
+                               Value="{Binding Value, ElementName=Component3Slider}"
+                               IsVisible="{TemplateBinding IsComponentTextInputVisible}" />
+                <primitives:ColorSlider x:Name="Component3Slider"
+                                        Grid.Column="2"
+                                        Grid.Row="4"
+                                        Orientation="Horizontal"
+                                        IsRoundingEnabled="True"
+                                        IsSnapToTickEnabled="True"
+                                        TickFrequency="1"
+                                        ColorComponent="Component3"
+                                        ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
+                                        HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
+                                        HorizontalAlignment="Stretch"
+                                        VerticalAlignment="Center"
+                                        IsVisible="{TemplateBinding IsComponentSliderVisible}" />
+                <!-- Alpha Component -->
+                <Border Grid.Column="0"
+                        Grid.Row="5"
+                        Width="{DynamicResource ColorViewComponentLabelWidth}"
+                        Height="{Binding ElementName=AlphaComponentNumericUpDown, Path=Bounds.Height}"
+                        Background="{DynamicResource TextControlBackgroundDisabled}"
+                        BorderBrush="{DynamicResource TextControlBorderBrush}"
+                        BorderThickness="1,1,0,1"
+                        CornerRadius="4,0,0,4"
+                        VerticalAlignment="Center"
+                        IsEnabled="{TemplateBinding IsAlphaEnabled}">
+                  <TextBlock x:Name="AlphaComponentTextBlock"
+                             Foreground="{DynamicResource TextControlForegroundDisabled}"
+                             FontWeight="SemiBold"
+                             Text="A"
+                             HorizontalAlignment="Center"
+                             VerticalAlignment="Center" />
+                  <Border.IsVisible>
+                    <MultiBinding Converter="{x:Static BoolConverters.And}">
+                      <Binding RelativeSource="{RelativeSource TemplatedParent}"
+                               Path="IsAlphaVisible" />
+                      <Binding RelativeSource="{RelativeSource TemplatedParent}"
+                               Path="IsComponentTextInputVisible" />
+                    </MultiBinding>
+                  </Border.IsVisible>
+                </Border>
+                <NumericUpDown x:Name="AlphaComponentNumericUpDown"
+                               Grid.Column="1"
+                               Grid.Row="5"
+                               AllowSpin="True"
+                               ShowButtonSpinner="False"
+                               Height="32"
+                               Width="{DynamicResource ColorViewComponentTextInputWidth}"
+                               CornerRadius="0,4,4,0"
+                               Margin="0,0,12,0"
+                               VerticalAlignment="Center"
+                               NumberFormat="{StaticResource ColorViewComponentNumberFormat}"
+                               Minimum="{Binding Minimum, ElementName=AlphaComponentSlider}"
+                               Maximum="{Binding Maximum, ElementName=AlphaComponentSlider}"
+                               Value="{Binding Value, ElementName=AlphaComponentSlider}"
+                               IsEnabled="{TemplateBinding IsAlphaEnabled}">
+                  <NumericUpDown.IsVisible>
+                    <MultiBinding Converter="{x:Static BoolConverters.And}">
+                      <Binding RelativeSource="{RelativeSource TemplatedParent}"
+                               Path="IsAlphaVisible" />
+                      <Binding RelativeSource="{RelativeSource TemplatedParent}"
+                               Path="IsComponentTextInputVisible" />
+                    </MultiBinding>
+                  </NumericUpDown.IsVisible>
+                </NumericUpDown>
+                <primitives:ColorSlider x:Name="AlphaComponentSlider"
+                                        Grid.Column="2"
+                                        Grid.Row="5"
+                                        Orientation="Horizontal"
+                                        IsRoundingEnabled="True"
+                                        IsSnapToTickEnabled="True"
+                                        TickFrequency="1"
+                                        ColorComponent="Alpha"
+                                        ColorModel="{TemplateBinding ColorModel, Mode=OneWay}"
+                                        HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
+                                        HorizontalAlignment="Stretch"
+                                        VerticalAlignment="Center"
+                                        IsEnabled="{TemplateBinding IsAlphaEnabled}">
+                  <primitives:ColorSlider.IsVisible>
+                    <MultiBinding Converter="{x:Static BoolConverters.And}">
+                      <Binding RelativeSource="{RelativeSource TemplatedParent}"
+                               Path="IsAlphaVisible" />
+                      <Binding RelativeSource="{RelativeSource TemplatedParent}"
+                               Path="IsComponentSliderVisible" />
+                    </MultiBinding>
+                  </primitives:ColorSlider.IsVisible>
+                </primitives:ColorSlider>
+              </Grid>
+            </TabItem>
+          </TabControl>
+          <!-- Previewer -->
+          <!-- Note that top/bottom margins have -5 to remove for drop shadow padding -->
+          <primitives:ColorPreviewer Grid.Row="1"
+                                     HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
+                                     IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
+                                     Margin="12,-5,12,7"
+                                     IsVisible="{TemplateBinding IsColorPreviewVisible}" />
+        </Grid>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+</Styles>

+ 18 - 2
src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml

@@ -1,8 +1,10 @@
 <Styles xmlns="https://github.com/avaloniaui"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:converters="using:Avalonia.Controls.Converters">
 
   <Styles.Resources>
-    <VisualBrush x:Key="CheckeredBackgroundBrush"
+    <!-- Shared Resources -->
+    <VisualBrush x:Key="ColorControlCheckeredBackgroundBrush"
                  TileMode="Tile"
                  Stretch="Uniform"
                  DestinationRect="0,0,8,8">
@@ -18,6 +20,16 @@
         </DrawingPresenter>
       </VisualBrush.Visual>
     </VisualBrush>
+    
+    <!-- Shared Converters -->
+    <converters:EnumToBoolConverter x:Key="EnumToBoolConverter" />
+    <converters:ToBrushConverter x:Key="ToBrushConverter" />
+    <converters:CornerRadiusFilterConverter x:Key="LeftCornerRadiusFilterConverter" Filter="TopLeft, BottomLeft"/>
+    <converters:CornerRadiusFilterConverter x:Key="RightCornerRadiusFilterConverter" Filter="TopRight, BottomRight"/>
+    <converters:CornerRadiusFilterConverter x:Key="TopCornerRadiusFilterConverter" Filter="TopLeft, TopRight"/>
+    <converters:CornerRadiusFilterConverter x:Key="BottomCornerRadiusFilterConverter" Filter="BottomLeft, BottomRight"/>
+    <converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadiusConverter" Corner="TopLeft" />
+    <converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadiusConverter" Corner="BottomRight" />
   </Styles.Resources>
 
   <!-- Primitives -->
@@ -25,4 +37,8 @@
   <StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml" />
   <StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml" />
 
+  <!-- Controls -->
+  <StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml" />
+  <StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml" />
+
 </Styles>

+ 56 - 0
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
+{
+    /// <summary>
+    /// 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.
+    /// </summary>
+    /// <remarks>
+    /// This converter is useful to enable binding of radio buttons with a selected enum value.
+    /// </remarks>
+    public class EnumToBoolConverter : IValueConverter
+    {
+        /// <inheritdoc/>
+        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);
+            }
+        }
+
+        /// <inheritdoc/>
+        public object? ConvertBack(
+            object? value,
+            Type targetType,
+            object? parameter,
+            CultureInfo culture)
+        {
+            if (value is bool boolValue &&
+                boolValue == true)
+            {
+                return parameter;
+            }
+
+            return BindingOperations.DoNothing;
+        }
+    }
+}

+ 0 - 54
src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs

@@ -1,54 +0,0 @@
-using System;
-using System.Globalization;
-using Avalonia.Data.Converters;
-
-namespace Avalonia.Controls.Converters
-{
-    /// <summary>
-    /// Converter that checks if an enum value is equal to the given parameter enum value.
-    /// </summary>
-    public class EnumValueEqualsConverter : IValueConverter
-    {
-        /// <inheritdoc/>
-        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;
-        }
-
-        /// <inheritdoc/>
-        public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
-        {
-            throw new System.NotImplementedException();
-        }
-    }
-}

+ 2 - 0
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@@ -291,7 +291,9 @@ namespace Avalonia.Controls.Primitives
                     
                     // 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);