diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index c0c83d6a351..1590be25ba6 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -13,8 +13,14 @@ - - + + + - - - - - + diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs new file mode 100644 index 00000000000..130d7e0eddf --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs @@ -0,0 +1,284 @@ +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implements a reduced flat design or flat UI color palette. + /// + /// + /// See: + /// - https://htmlcolorcodes.com/color-chart/ + /// - https://htmlcolorcodes.com/color-chart/flat-design-color-chart/ + /// - http://designmodo.github.io/Flat-UI/ + /// + /// The GitHub project is licensed as MIT: https://github.com/designmodo/Flat-UI. + /// + /// + public class FlatColorPalette : IColorPalette + { + // The full Flat UI color chart has 10 rows and 20 columns + // See: https://htmlcolorcodes.com/assets/downloads/flat-design-colors/flat-design-color-chart.png + // This is a reduced palette for usability + private static Color[,] colorChart = new Color[,] + { + // Pomegranate + { + Color.FromArgb(0xFF, 0xF9, 0xEB, 0xEA), + Color.FromArgb(0xFF, 0xE6, 0xB0, 0xAA), + Color.FromArgb(0xFF, 0xCD, 0x61, 0x55), + Color.FromArgb(0xFF, 0xA9, 0x32, 0x26), + Color.FromArgb(0xFF, 0x7B, 0x24, 0x1C), + }, + + // Amethyst + { + Color.FromArgb(0xFF, 0xF5, 0xEE, 0xF8), + Color.FromArgb(0xFF, 0xD7, 0xBD, 0xE2), + Color.FromArgb(0xFF, 0xAF, 0x7A, 0xC5), + Color.FromArgb(0xFF, 0x88, 0x4E, 0xA0), + Color.FromArgb(0xFF, 0x63, 0x39, 0x74), + }, + + // Belize Hole + { + Color.FromArgb(0xFF, 0xEA, 0xF2, 0xF8), + Color.FromArgb(0xFF, 0xA9, 0xCC, 0xE3), + Color.FromArgb(0xFF, 0x54, 0x99, 0xC7), + Color.FromArgb(0xFF, 0x24, 0x71, 0xA3), + Color.FromArgb(0xFF, 0x1A, 0x52, 0x76), + }, + + // Turquoise + { + Color.FromArgb(0xFF, 0xE8, 0xF8, 0xF5), + Color.FromArgb(0xFF, 0xA3, 0xE4, 0xD7), + Color.FromArgb(0xFF, 0x48, 0xC9, 0xB0), + Color.FromArgb(0xFF, 0x17, 0xA5, 0x89), + Color.FromArgb(0xFF, 0x11, 0x78, 0x64), + }, + + // Nephritis + { + Color.FromArgb(0xFF, 0xE9, 0xF7, 0xEF), + Color.FromArgb(0xFF, 0xA9, 0xDF, 0xBF), + Color.FromArgb(0xFF, 0x52, 0xBE, 0x80), + Color.FromArgb(0xFF, 0x22, 0x99, 0x54), + Color.FromArgb(0xFF, 0x19, 0x6F, 0x3D), + }, + + // Sunflower + { + Color.FromArgb(0xFF, 0xFE, 0xF9, 0xE7), + Color.FromArgb(0xFF, 0xF9, 0xE7, 0x9F), + Color.FromArgb(0xFF, 0xF4, 0xD0, 0x3F), + Color.FromArgb(0xFF, 0xD4, 0xAC, 0x0D), + Color.FromArgb(0xFF, 0x9A, 0x7D, 0x0A), + }, + + // Carrot + { + Color.FromArgb(0xFF, 0xFD, 0xF2, 0xE9), + Color.FromArgb(0xFF, 0xF5, 0xCB, 0xA7), + Color.FromArgb(0xFF, 0xEB, 0x98, 0x4E), + Color.FromArgb(0xFF, 0xCA, 0x6F, 0x1E), + Color.FromArgb(0xFF, 0x93, 0x51, 0x16), + }, + + // Clouds + { + Color.FromArgb(0xFF, 0xFD, 0xFE, 0xFE), + Color.FromArgb(0xFF, 0xF7, 0xF9, 0xF9), + Color.FromArgb(0xFF, 0xF0, 0xF3, 0xF4), + Color.FromArgb(0xFF, 0xD0, 0xD3, 0xD4), + Color.FromArgb(0xFF, 0x97, 0x9A, 0x9A), + }, + + // Concrete + { + Color.FromArgb(0xFF, 0xF4, 0xF6, 0xF6), + Color.FromArgb(0xFF, 0xD5, 0xDB, 0xDB), + Color.FromArgb(0xFF, 0xAA, 0xB7, 0xB8), + Color.FromArgb(0xFF, 0x83, 0x91, 0x92), + Color.FromArgb(0xFF, 0x5F, 0x6A, 0x6A), + }, + + // Wet Asphalt + { + Color.FromArgb(0xFF, 0xEB, 0xED, 0xEF), + Color.FromArgb(0xFF, 0xAE, 0xB6, 0xBF), + Color.FromArgb(0xFF, 0x5D, 0x6D, 0x7E), + Color.FromArgb(0xFF, 0x2E, 0x40, 0x53), + Color.FromArgb(0xFF, 0x21, 0x2F, 0x3C), + }, + }; + + /// + /// Gets the index of the default shade of colors in this palette. + /// + public const int DefaultShadeIndex = 2; + + /// + /// The index in the color palette of the 'Pomegranate' color. + /// This index can correspond to multiple color shades. + /// + public const int PomegranateIndex = 0; + + /// + /// The index in the color palette of the 'Amethyst' color. + /// This index can correspond to multiple color shades. + /// + public const int AmethystIndex = 1; + + /// + /// The index in the color palette of the 'BelizeHole' color. + /// This index can correspond to multiple color shades. + /// + public const int BelizeHoleIndex = 2; + + /// + /// The index in the color palette of the 'Turquoise' color. + /// This index can correspond to multiple color shades. + /// + public const int TurquoiseIndex = 3; + + /// + /// The index in the color palette of the 'Nephritis' color. + /// This index can correspond to multiple color shades. + /// + public const int NephritisIndex = 4; + + /// + /// The index in the color palette of the 'Sunflower' color. + /// This index can correspond to multiple color shades. + /// + public const int SunflowerIndex = 5; + + /// + /// The index in the color palette of the 'Carrot' color. + /// This index can correspond to multiple color shades. + /// + public const int CarrotIndex = 6; + + /// + /// The index in the color palette of the 'Clouds' color. + /// This index can correspond to multiple color shades. + /// + public const int CloudsIndex = 7; + + /// + /// The index in the color palette of the 'Concrete' color. + /// This index can correspond to multiple color shades. + /// + public const int ConcreteIndex = 8; + + /// + /// The index in the color palette of the 'WetAsphalt' color. + /// This index can correspond to multiple color shades. + /// + public const int WetAsphaltIndex = 9; + + /// + public int ColorCount + { + // Table is transposed compared to the reference chart + get => colorChart.GetLength(0); + } + + /// + public int ShadeCount + { + // Table is transposed compared to the reference chart + get => colorChart.GetLength(1); + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFC0392B. + /// + public static Color Pomegranate + { + get => colorChart[PomegranateIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF9B59B6. + /// + public static Color Amethyst + { + get => colorChart[AmethystIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF2980B9. + /// + public static Color BelizeHole + { + get => colorChart[BelizeHoleIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF1ABC9C. + /// + public static Color Turquoise + { + get => colorChart[TurquoiseIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF27AE60. + /// + public static Color Nephritis + { + get => colorChart[NephritisIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFF1C40F. + /// + public static Color Sunflower + { + get => colorChart[SunflowerIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFE67E22. + /// + public static Color Carrot + { + get => colorChart[CarrotIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFECF0F1. + /// + public static Color Clouds + { + get => colorChart[CloudsIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF95A5A6. + /// + public static Color Concrete + { + get => colorChart[ConcreteIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF34495E. + /// + public static Color WetAsphalt + { + get => colorChart[WetAsphaltIndex, DefaultShadeIndex]; + } + + /// + public Color GetColor(int colorIndex, int shadeIndex) + { + // Table is transposed compared to the reference chart + return colorChart[ + MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0) - 1), + MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1) - 1)]; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs new file mode 100644 index 00000000000..013e69ce20d --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FluentColorPalette.cs @@ -0,0 +1,136 @@ +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implements the standard Windows 10 color palette. + /// + public class FluentColorPalette : IColorPalette + { + // Values were taken from the Settings App, Personalization > Colors which match with + // https://docs.microsoft.com/en-us/windows/uwp/whats-new/windows-docs-december-2017 + // + // The default ordering and grouping of colors was undesirable so was modified. + // Colors were transposed: the colors in rows within the Settings app became columns here. + // This is because columns in an IColorPalette generally should contain different shades of + // the same color. In the settings app this concept is somewhat loosely reversed. + // The first 'column' ordering, after being transposed, was then reversed so 'red' colors + // were near to each other. + // + // This new ordering most closely follows the Windows standard while: + // + // 1. Keeping colors in a 'spectrum' order + // 2. Keeping like colors next to each both in rows and columns + // (which is unique for the windows palette). + // For example, similar red colors are next to each other in both + // rows within the same column and rows within the column next to it. + // This follows a 'snake-like' pattern as illustrated below. + // 3. A downside of this ordering is colors don't follow strict 'shades' + // as in other palettes. + // + // The colors will be displayed in the below pattern. + // This pattern follows a spectrum while keeping like-colors near to one + // another across both rows and columns. + // + // ┌Red───┐ ┌Blue──┐ ┌Gray──┐ + // │ │ │ │ │ | + // │ │ │ │ │ | + // Yellow └Violet┘ └Green─┘ Brown + + private static Color[,] colorChart = new Color[,] + { + { + // Ordering reversed for this section only + Color.FromArgb(255, 255, 67, 67), /* #FF4343 */ + Color.FromArgb(255, 209, 52, 56), /* #D13438 */ + Color.FromArgb(255, 239, 105, 80), /* #EF6950 */ + Color.FromArgb(255, 218, 59, 1), /* #DA3B01 */ + Color.FromArgb(255, 202, 80, 16), /* #CA5010 */ + Color.FromArgb(255, 247, 99, 12), /* #F7630C */ + Color.FromArgb(255, 255, 140, 0), /* #FF8C00 */ + Color.FromArgb(255, 255, 185, 0), /* #FFB900 */ + }, + { + Color.FromArgb(255, 231, 72, 86), /* #E74856 */ + Color.FromArgb(255, 232, 17, 35), /* #E81123 */ + Color.FromArgb(255, 234, 0, 94), /* #EA005E */ + Color.FromArgb(255, 195, 0, 82), /* #C30052 */ + Color.FromArgb(255, 227, 0, 140), /* #E3008C */ + Color.FromArgb(255, 191, 0, 119), /* #BF0077 */ + Color.FromArgb(255, 194, 57, 179), /* #C239B3 */ + Color.FromArgb(255, 154, 0, 137), /* #9A0089 */ + }, + { + Color.FromArgb(255, 0, 120, 215), /* #0078D7 */ + Color.FromArgb(255, 0, 99, 177), /* #0063B1 */ + Color.FromArgb(255, 142, 140, 216), /* #8E8CD8 */ + Color.FromArgb(255, 107, 105, 214), /* #6B69D6 */ + Color.FromArgb(255, 135, 100, 184), /* #8764B8 */ + Color.FromArgb(255, 116, 77, 169), /* #744DA9 */ + Color.FromArgb(255, 177, 70, 194), /* #B146C2 */ + Color.FromArgb(255, 136, 23, 152), /* #881798 */ + }, + { + Color.FromArgb(255, 0, 153, 188), /* #0099BC */ + Color.FromArgb(255, 45, 125, 154), /* #2D7D9A */ + Color.FromArgb(255, 0, 183, 195), /* #00B7C3 */ + Color.FromArgb(255, 3, 131, 135), /* #038387 */ + Color.FromArgb(255, 0, 178, 148), /* #00B294 */ + Color.FromArgb(255, 1, 133, 116), /* #018574 */ + Color.FromArgb(255, 0, 204, 106), /* #00CC6A */ + Color.FromArgb(255, 16, 137, 62), /* #10893E */ + }, + { + Color.FromArgb(255, 122, 117, 116), /* #7A7574 */ + Color.FromArgb(255, 93, 90, 80), /* #5D5A58 */ + Color.FromArgb(255, 104, 118, 138), /* #68768A */ + Color.FromArgb(255, 81, 92, 107), /* #515C6B */ + Color.FromArgb(255, 86, 124, 115), /* #567C73 */ + Color.FromArgb(255, 72, 104, 96), /* #486860 */ + Color.FromArgb(255, 73, 130, 5), /* #498205 */ + Color.FromArgb(255, 16, 124, 16), /* #107C10 */ + }, + { + Color.FromArgb(255, 118, 118, 118), /* #767676 */ + Color.FromArgb(255, 76, 74, 72), /* #4C4A48 */ + Color.FromArgb(255, 105, 121, 126), /* #69797E */ + Color.FromArgb(255, 74, 84, 89), /* #4A5459 */ + Color.FromArgb(255, 100, 124, 100), /* #647C64 */ + Color.FromArgb(255, 82, 94, 84), /* #525E54 */ + Color.FromArgb(255, 132, 117, 69), /* #847545 */ + Color.FromArgb(255, 126, 115, 95), /* #7E735F */ + } + }; + + /// + /// Gets the total number of colors in this palette. + /// A color is not necessarily a single value and may be composed of several shades. + /// This has little meaning in this palette as colors are not strictly separated. + /// + /// + public int ColorCount + { + get => colorChart.GetLength(0); + } + + /// + /// Gets the total number of shades for each color in this palette. + /// Shades are usually a variation of the color lightening or darkening it. + /// This has little meaning in this palette as colors are not strictly separated by shade. + /// + /// + public int ShadeCount + { + get => colorChart.GetLength(1); + } + + /// + public Color GetColor(int colorIndex, int shadeIndex) + { + return colorChart[ + MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0) - 1), + MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1) - 1)]; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalettes/IColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/IColorPalette.cs new file mode 100644 index 00000000000..7c6ebc3f6a3 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/IColorPalette.cs @@ -0,0 +1,38 @@ +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Interface to define a color palette. + /// + public interface IColorPalette + { + /// + /// Gets the total number of colors in this palette. + /// A color is not necessarily a single value and may be composed of several shades. + /// + /// + /// Represents total columns in a table. + /// + int ColorCount { get; } + + /// + /// Gets the total number of shades for each color in this palette. + /// Shades are usually a variation of the color lightening or darkening it. + /// + /// + /// Represents total rows in a table. + /// + int ShadeCount { get; } + + /// + /// Gets a color in the palette by index. + /// + /// The index of the color in the palette. + /// The index must be between zero and . + /// The index of the color shade in the palette. + /// The index must be between zero and . + /// The color at the specified index or an exception. + Color GetColor(int colorIndex, int shadeIndex); + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalettes/SixteenColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/SixteenColorPalette.cs new file mode 100644 index 00000000000..f3abfdfd7fb --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/SixteenColorPalette.cs @@ -0,0 +1,302 @@ +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implements the standard sixteen color palette from the HTML 4.01 specification. + /// + /// + /// See https://en.wikipedia.org/wiki/Web_colors#HTML_color_names. + /// + public class SixteenColorPalette : IColorPalette + { + // The 16 standard colors from HTML and early Windows computers + // https://en.wikipedia.org/wiki/List_of_software_palettes + // https://en.wikipedia.org/wiki/Web_colors#HTML_color_names + private static Color[,] colorChart = new Color[,] + { + { + Colors.White, + Colors.Silver + }, + { + Colors.Gray, + Colors.Black + }, + { + Colors.Red, + Colors.Maroon + }, + { + Colors.Yellow, + Colors.Olive + }, + { + Colors.Lime, + Colors.Green + }, + { + Colors.Aqua, + Colors.Teal + }, + { + Colors.Blue, + Colors.Navy + }, + { + Colors.Fuchsia, + Colors.Purple + } + }; + + /// + /// Gets the index of the default shade of colors in this palette. + /// + public const int DefaultShadeIndex = 0; + + /// + /// The index in the color palette of the 'White' color. + /// This index can correspond to multiple color shades. + /// + public const int WhiteIndex = 0; + + /// + /// The index in the color palette of the 'Silver' color. + /// This index can correspond to multiple color shades. + /// + public const int SilverIndex = 1; + + /// + /// The index in the color palette of the 'Gray' color. + /// This index can correspond to multiple color shades. + /// + public const int GrayIndex = 2; + + /// + /// The index in the color palette of the 'Black' color. + /// This index can correspond to multiple color shades. + /// + public const int BlackIndex = 3; + + /// + /// The index in the color palette of the 'Red' color. + /// This index can correspond to multiple color shades. + /// + public const int RedIndex = 4; + + /// + /// The index in the color palette of the 'Maroon' color. + /// This index can correspond to multiple color shades. + /// + public const int MaroonIndex = 5; + + /// + /// The index in the color palette of the 'Yellow' color. + /// This index can correspond to multiple color shades. + /// + public const int YellowIndex = 6; + + /// + /// The index in the color palette of the 'Olive' color. + /// This index can correspond to multiple color shades. + /// + public const int OliveIndex = 7; + + /// + /// The index in the color palette of the 'Lime' color. + /// This index can correspond to multiple color shades. + /// + public const int LimeIndex = 8; + + /// + /// The index in the color palette of the 'Green' color. + /// This index can correspond to multiple color shades. + /// + public const int GreenIndex = 9; + + /// + /// The index in the color palette of the 'Aqua' color. + /// This index can correspond to multiple color shades. + /// + public const int AquaIndex = 10; + + /// + /// The index in the color palette of the 'Teal' color. + /// This index can correspond to multiple color shades. + /// + public const int TealIndex = 11; + + /// + /// The index in the color palette of the 'Blue' color. + /// This index can correspond to multiple color shades. + /// + public const int BlueIndex = 12; + + /// + /// The index in the color palette of the 'Navy' color. + /// This index can correspond to multiple color shades. + /// + public const int NavyIndex = 13; + + /// + /// The index in the color palette of the 'Fuchsia' color. + /// This index can correspond to multiple color shades. + /// + public const int FuchsiaIndex = 14; + + /// + /// The index in the color palette of the 'Purple' color. + /// This index can correspond to multiple color shades. + /// + public const int PurpleIndex = 15; + + /// + public int ColorCount + { + get => colorChart.GetLength(0); + } + + /// + public int ShadeCount + { + get => colorChart.GetLength(1); + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFFFFFF. + /// + public static Color White + { + get => colorChart[WhiteIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFC0C0C0. + /// + public static Color Silver + { + get => colorChart[SilverIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF808080. + /// + public static Color Gray + { + get => colorChart[GrayIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF000000. + /// + public static Color Black + { + get => colorChart[BlackIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFF0000. + /// + public static Color Red + { + get => colorChart[RedIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF800000. + /// + public static Color Maroon + { + get => colorChart[MaroonIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFFFF00. + /// + public static Color Yellow + { + get => colorChart[YellowIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF808000. + /// + public static Color Olive + { + get => colorChart[OliveIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF00FF00. + /// + public static Color Lime + { + get => colorChart[LimeIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF008000. + /// + public static Color Green + { + get => colorChart[GreenIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF00FFFF. + /// + public static Color Aqua + { + get => colorChart[AquaIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF008080. + /// + public static Color Teal + { + get => colorChart[TealIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF0000FF. + /// + public static Color Blue + { + get => colorChart[BlueIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF000080. + /// + public static Color Navy + { + get => colorChart[NavyIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FFFF00FF. + /// + public static Color Fuchsia + { + get => colorChart[FuchsiaIndex, DefaultShadeIndex]; + } + + /// + /// Gets the palette defined color that has an ARGB value of #FF800080. + /// + public static Color Purple + { + get => colorChart[PurpleIndex, DefaultShadeIndex]; + } + + /// + public Color GetColor(int colorIndex, int shadeIndex) + { + return colorChart[ + MathUtilities.Clamp(colorIndex, 0, colorChart.GetLength(0) - 1), + MathUtilities.Clamp(shadeIndex, 0, colorChart.GetLength(1) - 1)]; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs new file mode 100644 index 00000000000..39369bcbdbf --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -0,0 +1,19 @@ +namespace Avalonia.Controls +{ + /// + /// Presents a color for user editing using a spectrum, palette and component sliders within a drop down. + /// Editing is available when the drop down flyout is opened; otherwise, only the preview color is shown. + /// + public class ColorPicker : ColorView + { + /// + /// Initializes a new instance of the class. + /// + public ColorPicker() : base() + { + // Completely ignore property changes here + // The ColorView in the control template is responsible to manage this + base.ignorePropertyChanged = true; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs index 0fa6ab80835..e1ffbb7eae0 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs @@ -16,11 +16,11 @@ public partial class ColorPreviewer defaultBindingMode: BindingMode.TwoWay); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty ShowAccentColorsProperty = + public static readonly StyledProperty IsAccentColorsVisibleProperty = AvaloniaProperty.Register( - nameof(ShowAccentColors), + nameof(IsAccentColorsVisible), true); /// @@ -38,13 +38,13 @@ public HsvColor HsvColor } /// - /// Gets or sets a value indicating whether accent colors are shown along + /// Gets or sets a value indicating whether accent colors are visible along /// with the preview color. /// - public bool ShowAccentColors + public bool IsAccentColorsVisible { - get => GetValue(ShowAccentColorsProperty); - set => SetValue(ShowAccentColorsProperty, value); + get => GetValue(IsAccentColorsVisibleProperty); + set => SetValue(IsAccentColorsVisibleProperty, value); } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs index d04ddf4bd6b..6f494305055 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs @@ -10,10 +10,10 @@ namespace Avalonia.Controls.Primitives /// /// Presents a preview color with optional accent colors. /// - [TemplatePart(Name = nameof(AccentDec1Border), Type = typeof(Border))] - [TemplatePart(Name = nameof(AccentDec2Border), Type = typeof(Border))] - [TemplatePart(Name = nameof(AccentInc1Border), Type = typeof(Border))] - [TemplatePart(Name = nameof(AccentInc2Border), Type = typeof(Border))] + [TemplatePart("PART_AccentDecrement1Border", typeof(Border))] + [TemplatePart("PART_AccentDecrement2Border", typeof(Border))] + [TemplatePart("PART_AccentIncrement1Border", typeof(Border))] + [TemplatePart("PART_AccentIncrement2Border", typeof(Border))] public partial class ColorPreviewer : TemplatedControl { /// @@ -24,10 +24,11 @@ public partial class ColorPreviewer : TemplatedControl private bool eventsConnected = false; - private Border? AccentDec1Border; - private Border? AccentDec2Border; - private Border? AccentInc1Border; - private Border? AccentInc2Border; + // XAML template parts + private Border? _accentDecrement1Border; + private Border? _accentDecrement2Border; + private Border? _accentIncrement1Border; + private Border? _accentIncrement2Border; /// /// Initializes a new instance of the class. @@ -45,20 +46,20 @@ private void ConnectEvents(bool connected) if (connected == true && eventsConnected == false) { // Add all events - if (AccentDec1Border != null) { AccentDec1Border.PointerPressed += AccentBorder_PointerPressed; } - if (AccentDec2Border != null) { AccentDec2Border.PointerPressed += AccentBorder_PointerPressed; } - if (AccentInc1Border != null) { AccentInc1Border.PointerPressed += AccentBorder_PointerPressed; } - if (AccentInc2Border != null) { AccentInc2Border.PointerPressed += AccentBorder_PointerPressed; } + if (_accentDecrement1Border != null) { _accentDecrement1Border.PointerPressed += AccentBorder_PointerPressed; } + if (_accentDecrement2Border != null) { _accentDecrement2Border.PointerPressed += AccentBorder_PointerPressed; } + if (_accentIncrement1Border != null) { _accentIncrement1Border.PointerPressed += AccentBorder_PointerPressed; } + if (_accentIncrement2Border != null) { _accentIncrement2Border.PointerPressed += AccentBorder_PointerPressed; } eventsConnected = true; } else if (connected == false && eventsConnected == true) { // Remove all events - if (AccentDec1Border != null) { AccentDec1Border.PointerPressed -= AccentBorder_PointerPressed; } - if (AccentDec2Border != null) { AccentDec2Border.PointerPressed -= AccentBorder_PointerPressed; } - if (AccentInc1Border != null) { AccentInc1Border.PointerPressed -= AccentBorder_PointerPressed; } - if (AccentInc2Border != null) { AccentInc2Border.PointerPressed -= AccentBorder_PointerPressed; } + if (_accentDecrement1Border != null) { _accentDecrement1Border.PointerPressed -= AccentBorder_PointerPressed; } + if (_accentDecrement2Border != null) { _accentDecrement2Border.PointerPressed -= AccentBorder_PointerPressed; } + if (_accentIncrement1Border != null) { _accentIncrement1Border.PointerPressed -= AccentBorder_PointerPressed; } + if (_accentIncrement2Border != null) { _accentIncrement2Border.PointerPressed -= AccentBorder_PointerPressed; } eventsConnected = false; } @@ -70,10 +71,10 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) // Remove any existing events present if the control was previously loaded then unloaded ConnectEvents(false); - AccentDec1Border = e.NameScope.Find(nameof(AccentDec1Border)); - AccentDec2Border = e.NameScope.Find(nameof(AccentDec2Border)); - AccentInc1Border = e.NameScope.Find(nameof(AccentInc1Border)); - AccentInc2Border = e.NameScope.Find(nameof(AccentInc2Border)); + _accentDecrement1Border = e.NameScope.Find("PART_AccentDecrement1Border"); + _accentDecrement2Border = e.NameScope.Find("PART_AccentDecrement2Border"); + _accentIncrement1Border = e.NameScope.Find("PART_AccentIncrement1Border"); + _accentIncrement2Border = e.NameScope.Find("PART_AccentIncrement2Border"); // Must connect after controls are found ConnectEvents(true); @@ -116,15 +117,15 @@ private void AccentBorder_PointerPressed(object? sender, PointerPressedEventArgs // Get the value component delta try { - accentStep = int.Parse(border?.Tag?.ToString() ?? "", CultureInfo.InvariantCulture); + accentStep = int.Parse(border?.Tag?.ToString() ?? "0", CultureInfo.InvariantCulture); } catch { } - HsvColor newHsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep); - HsvColor oldHsvColor = HsvColor; - - HsvColor = newHsvColor; - OnColorChanged(new ColorChangedEventArgs(oldHsvColor.ToRgb(), newHsvColor.ToRgb())); + if (accentStep != 0) + { + // ColorChanged will be invoked in OnPropertyChanged if the value is different + HsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep); + } } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs index 31bd296288f..e2a34a7f909 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs @@ -49,12 +49,12 @@ public partial class ColorSlider true); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty IsAutoUpdatingEnabledProperty = + public static readonly StyledProperty IsRoundingEnabledProperty = AvaloniaProperty.Register( - nameof(IsAutoUpdatingEnabled), - true); + nameof(IsRoundingEnabled), + false); /// /// Defines the property. @@ -120,16 +120,16 @@ public bool IsAlphaMaxForced } /// - /// Gets or sets a value indicating whether automatic background and foreground updates will be - /// calculated when the set color changes. + /// Gets or sets a value indicating whether rounding of color component values is enabled. /// /// - /// This can be disabled for performance reasons when working with multiple sliders. + /// This is applicable for the HSV color model only. The struct uses double + /// values while the struct uses byte. Only double types need rounding. /// - public bool IsAutoUpdatingEnabled + public bool IsRoundingEnabled { - get => GetValue(IsAutoUpdatingEnabledProperty); - set => SetValue(IsAutoUpdatingEnabledProperty, value); + get => GetValue(IsRoundingEnabledProperty); + set => SetValue(IsRoundingEnabledProperty, value); } /// diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 3c38c6ed1b6..b662d202237 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -20,8 +20,16 @@ public partial class ColorSlider : Slider /// public event EventHandler? ColorChanged; - private const double MaxHue = 359.99999999999999999; // 17 decimal places - private bool disableUpdates = false; + /// + /// Defines the maximum hue component value + /// (other components are always 0..100 or 0.255). + /// + /// + /// This should match the default property. + /// + private const double MaxHue = 359; + + protected bool ignorePropertyChanged = false; /// /// Initializes a new instance of the class. @@ -107,21 +115,41 @@ private async void UpdateBackground() } } + /// + /// Rounds the component values of the given . + /// This is useful for user-display and to ensure a color matches user selection exactly. + /// + /// The to round component values for. + /// A new with rounded component values. + private HsvColor RoundComponentValues(HsvColor hsvColor) + { + return new HsvColor( + Math.Round(hsvColor.A, 2, MidpointRounding.AwayFromZero), + Math.Round(hsvColor.H, 0, MidpointRounding.AwayFromZero), + Math.Round(hsvColor.S, 2, MidpointRounding.AwayFromZero), + Math.Round(hsvColor.V, 2, MidpointRounding.AwayFromZero)); + } + /// /// Updates the slider property values by applying the current color. /// /// /// Warning: This will trigger property changed updates. - /// Consider using externally. + /// Consider using externally. /// private void SetColorToSliderValues() { - var hsvColor = HsvColor; - var rgbColor = Color; var component = ColorComponent; if (ColorModel == ColorModel.Hsva) { + var hsvColor = HsvColor; + + if (IsRoundingEnabled) + { + hsvColor = RoundComponentValues(hsvColor); + } + // Note: Components converted into a usable range for the user switch (component) { @@ -149,6 +177,8 @@ private void SetColorToSliderValues() } else { + var rgbColor = Color; + switch (component) { case ColorComponent.Alpha: @@ -183,13 +213,12 @@ private void SetColorToSliderValues() HsvColor hsvColor = new HsvColor(); Color rgbColor = new Color(); double sliderPercent = Value / (Maximum - Minimum); - - var baseHsvColor = HsvColor; - var baseRgbColor = Color; var component = ColorComponent; if (ColorModel == ColorModel.Hsva) { + var baseHsvColor = HsvColor; + switch (component) { case ColorComponent.Alpha: @@ -214,10 +243,12 @@ private void SetColorToSliderValues() } } - return (hsvColor.ToRgb(), hsvColor); + rgbColor = hsvColor.ToRgb(); } else { + var baseRgbColor = Color; + byte componentValue = Convert.ToByte(MathUtilities.Clamp(sliderPercent * 255, 0, 255)); switch (component) @@ -236,8 +267,15 @@ private void SetColorToSliderValues() break; } - return (rgbColor, rgbColor.ToHsv()); + hsvColor = rgbColor.ToHsv(); } + + if (IsRoundingEnabled) + { + hsvColor = RoundComponentValues(hsvColor); + } + + return (rgbColor, hsvColor); } /// @@ -306,7 +344,7 @@ private Color GetEquivalentBackgroundColor(Color rgbColor) /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (disableUpdates) + if (ignorePropertyChanged) { base.OnPropertyChanged(change); return; @@ -315,54 +353,59 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang // Always keep the two color properties in sync if (change.Property == ColorProperty) { - disableUpdates = true; + ignorePropertyChanged = true; HsvColor = Color.ToHsv(); - if (IsAutoUpdatingEnabled) - { - SetColorToSliderValues(); - UpdateBackground(); - } - + SetColorToSliderValues(); + UpdateBackground(); UpdatePseudoClasses(); + OnColorChanged(new ColorChangedEventArgs( change.GetOldValue(), change.GetNewValue())); - disableUpdates = false; + ignorePropertyChanged = false; + } + else if (change.Property == ColorModelProperty) + { + ignorePropertyChanged = true; + + SetColorToSliderValues(); + UpdateBackground(); + UpdatePseudoClasses(); + + ignorePropertyChanged = false; } else if (change.Property == HsvColorProperty) { - disableUpdates = true; + ignorePropertyChanged = true; Color = HsvColor.ToRgb(); - if (IsAutoUpdatingEnabled) - { - SetColorToSliderValues(); - UpdateBackground(); - } - + SetColorToSliderValues(); + UpdateBackground(); UpdatePseudoClasses(); + OnColorChanged(new ColorChangedEventArgs( change.GetOldValue().ToRgb(), change.GetNewValue().ToRgb())); - disableUpdates = false; + ignorePropertyChanged = false; + } + else if (change.Property == IsRoundingEnabledProperty) + { + SetColorToSliderValues(); } else if (change.Property == BoundsProperty) { - if (IsAutoUpdatingEnabled) - { - UpdateBackground(); - } + UpdateBackground(); } else if (change.Property == ValueProperty || change.Property == MinimumProperty || change.Property == MaximumProperty) { - disableUpdates = true; + ignorePropertyChanged = true; Color oldColor = Color; (var color, var hsvColor) = GetColorFromSliderValues(); @@ -381,7 +424,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang UpdatePseudoClasses(); OnColorChanged(new ColorChangedEventArgs(oldColor, Color)); - disableUpdates = false; + ignorePropertyChanged = false; } base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index 587a89ee38a..00d84f5dd3e 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -97,7 +97,7 @@ public partial class ColorSpectrum /// Gets or sets the currently selected color in the RGB color model. /// /// - /// For control authors use instead to avoid loss + /// For control authors, use instead to avoid loss /// of precision and color drifting. /// public Color Color diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 7e6b70a1465..bd44161a42a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -44,7 +44,6 @@ public partial class ColorSpectrum : TemplatedControl private bool _updatingColor = false; private bool _updatingHsvColor = false; - private bool _isPointerOver = false; private bool _isPointerPressed = false; private bool _shouldShowLargeSelection = false; private List _hsvValues = new List(); @@ -851,7 +850,6 @@ private void UpdateEllipse() /// private void InputTarget_PointerEntered(object? sender, PointerEventArgs args) { - _isPointerOver = true; UpdatePseudoClasses(); args.Handled = true; } @@ -859,7 +857,6 @@ private void InputTarget_PointerEntered(object? sender, PointerEventArgs args) /// private void InputTarget_PointerExited(object? sender, PointerEventArgs args) { - _isPointerOver = false; UpdatePseudoClasses(); args.Handled = true; } diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs new file mode 100644 index 00000000000..b76059037b5 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -0,0 +1,495 @@ +using System.Collections.Generic; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + public partial class ColorView + { + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + nameof(Color), + Colors.White, + defaultBindingMode: BindingMode.TwoWay, + coerce: CoerceColor) ; + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorModelProperty = + AvaloniaProperty.Register( + nameof(ColorModel), + ColorModel.Rgba); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorSpectrumComponentsProperty = + AvaloniaProperty.Register( + nameof(ColorSpectrumComponents), + ColorSpectrumComponents.HueSaturation); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorSpectrumShapeProperty = + AvaloniaProperty.Register( + nameof(ColorSpectrumShape), + ColorSpectrumShape.Box); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.White.ToHsv(), + defaultBindingMode: BindingMode.TwoWay, + coerce: CoerceHsvColor); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAccentColorsVisibleProperty = + AvaloniaProperty.Register( + nameof(IsAccentColorsVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaEnabledProperty = + AvaloniaProperty.Register( + nameof(IsAlphaEnabled), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaVisibleProperty = + AvaloniaProperty.Register( + nameof(IsAlphaVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorComponentsVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorComponentsVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorModelVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorModelVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorPaletteVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorPaletteVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorPreviewVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorPreviewVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorSpectrumVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorSpectrumVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsColorSpectrumSliderVisibleProperty = + AvaloniaProperty.Register( + nameof(IsColorSpectrumSliderVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsComponentSliderVisibleProperty = + AvaloniaProperty.Register( + nameof(IsComponentSliderVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsComponentTextInputVisibleProperty = + AvaloniaProperty.Register( + nameof(IsComponentTextInputVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsHexInputVisibleProperty = + AvaloniaProperty.Register( + nameof(IsHexInputVisible), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxHueProperty = + AvaloniaProperty.Register( + nameof(MaxHue), + 359); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxSaturationProperty = + AvaloniaProperty.Register( + nameof(MaxSaturation), + 100); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxValueProperty = + AvaloniaProperty.Register( + nameof(MaxValue), + 100); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinHueProperty = + AvaloniaProperty.Register( + nameof(MinHue), + 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinSaturationProperty = + AvaloniaProperty.Register( + nameof(MinSaturation), + 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinValueProperty = + AvaloniaProperty.Register( + nameof(MinValue), + 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty?> PaletteColorsProperty = + AvaloniaProperty.Register?>( + nameof(PaletteColors), + null); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PaletteColumnCountProperty = + AvaloniaProperty.Register( + nameof(PaletteColumnCount), + 4); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PaletteProperty = + AvaloniaProperty.Register( + nameof(Palette), + null); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SelectedIndexProperty = + AvaloniaProperty.Register( + nameof(SelectedIndex), + (int)ColorViewTab.Spectrum); + + /// + public Color Color + { + get => GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + /// + /// + /// This property is only applicable to the components tab. + /// The spectrum tab must always be in HSV and the palette tab contains only pre-defined colors. + /// + public ColorModel ColorModel + { + get => GetValue(ColorModelProperty); + set => SetValue(ColorModelProperty, value); + } + + /// + public ColorSpectrumComponents ColorSpectrumComponents + { + get => GetValue(ColorSpectrumComponentsProperty); + set => SetValue(ColorSpectrumComponentsProperty, value); + } + + /// + public ColorSpectrumShape ColorSpectrumShape + { + get => GetValue(ColorSpectrumShapeProperty); + set => SetValue(ColorSpectrumShapeProperty, value); + } + + /// + public HsvColor HsvColor + { + get => GetValue(HsvColorProperty); + set => SetValue(HsvColorProperty, value); + } + + /// + public bool IsAccentColorsVisible + { + get => GetValue(IsAccentColorsVisibleProperty); + set => SetValue(IsAccentColorsVisibleProperty, value); + } + + /// + /// Gets or sets a value indicating whether the alpha component is enabled. + /// When disabled (set to false) the alpha component will be fixed to maximum and + /// editing controls disabled. + /// + public bool IsAlphaEnabled + { + get => GetValue(IsAlphaEnabledProperty); + set => SetValue(IsAlphaEnabledProperty, value); + } + + /// + /// Gets or sets a value indicating whether the alpha component editing controls + /// (Slider(s) and TextBox) are visible. When hidden, the existing alpha component + /// value is maintained. + /// + /// + /// Note that also controls the alpha + /// component TextBox visibility. + /// + public bool IsAlphaVisible + { + get => GetValue(IsAlphaVisibleProperty); + set => SetValue(IsAlphaVisibleProperty, value); + } + + /// + /// Gets or sets a value indicating whether the color components tab/panel/page (subview) is visible. + /// + public bool IsColorComponentsVisible + { + get => GetValue(IsColorComponentsVisibleProperty); + set => SetValue(IsColorComponentsVisibleProperty, value); + } + + /// + /// Gets or sets a value indicating whether the active color model indicator/selector is visible. + /// + public bool IsColorModelVisible + { + get => GetValue(IsColorModelVisibleProperty); + set => SetValue(IsColorModelVisibleProperty, value); + } + + /// + /// Gets or sets a value indicating whether the color palette tab/panel/page (subview) is visible. + /// + public bool IsColorPaletteVisible + { + get => GetValue(IsColorPaletteVisibleProperty); + set => SetValue(IsColorPaletteVisibleProperty, value); + } + + /// + /// Gets or sets a value indicating whether the color preview is visible. + /// + /// + /// Note that accent color visibility is controlled separately by + /// . + /// + public bool IsColorPreviewVisible + { + get => GetValue(IsColorPreviewVisibleProperty); + set => SetValue(IsColorPreviewVisibleProperty, value); + } + + /// + /// Gets or sets a value indicating whether the color spectrum tab/panel/page (subview) is visible. + /// + public bool IsColorSpectrumVisible + { + get => GetValue(IsColorSpectrumVisibleProperty); + set => SetValue(IsColorSpectrumVisibleProperty, value); + } + + /// + /// Gets or sets a value indicating whether the color spectrum's third component slider + /// is visible. + /// + public bool IsColorSpectrumSliderVisible + { + get => GetValue(IsColorSpectrumSliderVisibleProperty); + set => SetValue(IsColorSpectrumSliderVisibleProperty, value); + } + + /// + /// Gets or sets a value indicating whether color component sliders are visible. + /// + /// + /// All color components are controlled by this property but alpha can also be + /// controlled with . + /// + public bool IsComponentSliderVisible + { + get => GetValue(IsComponentSliderVisibleProperty); + set => SetValue(IsComponentSliderVisibleProperty, value); + } + + /// + /// Gets or sets a value indicating whether color component text inputs are visible. + /// + /// + /// All color components are controlled by this property but alpha can also be + /// controlled with . + /// + public bool IsComponentTextInputVisible + { + get => GetValue(IsComponentTextInputVisibleProperty); + set => SetValue(IsComponentTextInputVisibleProperty, value); + } + + /// + /// Gets or sets a value indicating whether the hexadecimal color value text input + /// is visible. + /// + public bool IsHexInputVisible + { + get => GetValue(IsHexInputVisibleProperty); + set => SetValue(IsHexInputVisibleProperty, value); + } + + /// + public int MaxHue + { + get => GetValue(MaxHueProperty); + set => SetValue(MaxHueProperty, value); + } + + /// + public int MaxSaturation + { + get => GetValue(MaxSaturationProperty); + set => SetValue(MaxSaturationProperty, value); + } + + /// + public int MaxValue + { + get => GetValue(MaxValueProperty); + set => SetValue(MaxValueProperty, value); + } + + /// + public int MinHue + { + get => GetValue(MinHueProperty); + set => SetValue(MinHueProperty, value); + } + + /// + public int MinSaturation + { + get => GetValue(MinSaturationProperty); + set => SetValue(MinSaturationProperty, value); + } + + /// + public int MinValue + { + get => GetValue(MinValueProperty); + set => SetValue(MinValueProperty, value); + } + + /// + /// Gets or sets the collection of individual colors in the palette. + /// + /// + /// This is not commonly set manually. Instead, it should be set automatically by + /// providing an to the property. + ///

+ /// Also note that this property is what should be bound in the control template. + /// is too high-level to use on its own. + ///
+ public IEnumerable? PaletteColors + { + get => GetValue(PaletteColorsProperty); + set => SetValue(PaletteColorsProperty, value); + } + + /// + /// Gets or sets the number of colors in each row (section) of the color palette. + /// Within a standard palette, rows are shades and columns are colors. + /// + /// + /// This is not commonly set manually. Instead, it should be set automatically by + /// providing an to the property. + ///

+ /// Also note that this property is what should be bound in the control template. + /// is too high-level to use on its own. + ///
+ public int PaletteColumnCount + { + get => GetValue(PaletteColumnCountProperty); + set => SetValue(PaletteColumnCountProperty, value); + } + + /// + /// Gets or sets the color palette. + /// + /// + /// This will automatically set both and + /// overwriting any existing values. + /// + public IColorPalette? Palette + { + get => GetValue(PaletteProperty); + set => SetValue(PaletteProperty, value); + } + + /// + /// Gets or sets the index of the selected tab/panel/page (subview). + /// + public int SelectedIndex + { + get => GetValue(SelectedIndexProperty); + set => SetValue(SelectedIndexProperty, value); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs new file mode 100644 index 00000000000..89f1afb1acd --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -0,0 +1,379 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using Avalonia.Controls.Converters; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Media; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Controls +{ + /// + /// Presents a color for user editing using a spectrum, palette and component sliders. + /// + [TemplatePart("PART_HexTextBox", typeof(TextBox))] + [TemplatePart("PART_TabControl", typeof(TabControl))] + public partial class ColorView : TemplatedControl + { + /// + /// Event for when the selected color changes within the slider. + /// + public event EventHandler? ColorChanged; + + // XAML template parts + private TextBox? _hexTextBox; + private TabControl? _tabControl; + + private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); + protected bool ignorePropertyChanged = false; + + /// + /// Initializes a new instance of the class. + /// + public ColorView() : base() + { + } + + /// + /// Gets the value of the hex TextBox and sets it as the current . + /// If invalid, the TextBox hex text will revert back to the last valid color. + /// + private void GetColorFromHexTextBox() + { + if (_hexTextBox != null) + { + var convertedColor = colorToHexConverter.ConvertBack(_hexTextBox.Text, typeof(Color), null, CultureInfo.CurrentCulture); + + if (convertedColor is Color color) + { + Color = color; + } + + // Re-apply the hex value + // This ensure the hex color value is always valid and formatted correctly + SetColorToHexTextBox(); + } + } + + /// + /// Sets the current to the hex TextBox. + /// + private void SetColorToHexTextBox() + { + if (_hexTextBox != null) + { + _hexTextBox.Text = colorToHexConverter.Convert(Color, typeof(string), null, CultureInfo.CurrentCulture) as string; + } + } + + /// + /// Validates the tab/panel/page selection taking into account the visibility of each item + /// as well as the current selection. + /// + /// + /// Derived controls may re-implement this based on their default style / control template + /// and any specialized selection needs. + /// + protected virtual void ValidateSelection() + { + if (_tabControl != null && + _tabControl.Items != null) + { + // Determine the number of visible tab items + int numVisibleItems = 0; + foreach (var item in _tabControl.Items) + { + if (item is Control control && + control.IsVisible) + { + numVisibleItems++; + } + } + + // Verify the selection + if (numVisibleItems > 0) + { + object? selectedItem = null; + + if (_tabControl.SelectedItem == null && + _tabControl.ItemCount > 0) + { + // As a failsafe, forcefully select the first item + foreach (var item in _tabControl.Items) + { + selectedItem = item; + break; + } + } + else + { + selectedItem = _tabControl.SelectedItem; + } + + if (selectedItem is Control selectedControl && + selectedControl.IsVisible == false) + { + // Select the first visible item instead + foreach (var item in _tabControl.Items) + { + if (item is Control control && + control.IsVisible) + { + selectedItem = item; + break; + } + } + } + + _tabControl.SelectedItem = selectedItem; + _tabControl.IsVisible = true; + } + else + { + // Special case when all items are hidden + // If TabControl ever properly supports no selected item / + // all items hidden this can be removed + _tabControl.SelectedItem = null; + _tabControl.IsVisible = false; + } + + // Hide the "tab strip" if there is only one tab + // This allows, for example, to view only the palette + /* + var itemsPresenter = _tabControl.FindDescendantOfType(); + if (itemsPresenter != null) + { + if (numVisibleItems == 1) + { + itemsPresenter.IsVisible = false; + } + else + { + itemsPresenter.IsVisible = true; + } + } + */ + + // Note that if externally the SelectedIndex is set to 4 or something + // outside the valid range, the TabControl will ignore it and replace it + // with a valid SelectedIndex. This however is not propagated back through + // the TwoWay binding in the control template so the SelectedIndex and + // SelectedIndex become out of sync. + // + // The work-around for this is done here where SelectedIndex is forcefully + // synchronized with whatever the TabControl property value is. This is + // possible since selection validation is already done by this method. + SelectedIndex = _tabControl.SelectedIndex; + } + + return; + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + if (_hexTextBox != null) + { + _hexTextBox.KeyDown -= HexTextBox_KeyDown; + _hexTextBox.LostFocus -= HexTextBox_LostFocus; + } + + _hexTextBox = e.NameScope.Find("PART_HexTextBox"); + _tabControl = e.NameScope.Find("PART_TabControl"); + + SetColorToHexTextBox(); + + if (_hexTextBox != null) + { + _hexTextBox.KeyDown += HexTextBox_KeyDown; + _hexTextBox.LostFocus += HexTextBox_LostFocus; + } + + base.OnApplyTemplate(e); + ValidateSelection(); + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (ignorePropertyChanged) + { + base.OnPropertyChanged(change); + return; + } + + // Always keep the two color properties in sync + if (change.Property == ColorProperty) + { + ignorePropertyChanged = true; + + HsvColor = Color.ToHsv(); + SetColorToHexTextBox(); + + OnColorChanged(new ColorChangedEventArgs( + change.GetOldValue(), + change.GetNewValue())); + + ignorePropertyChanged = false; + } + else if (change.Property == HsvColorProperty) + { + ignorePropertyChanged = true; + + Color = HsvColor.ToRgb(); + SetColorToHexTextBox(); + + OnColorChanged(new ColorChangedEventArgs( + change.GetOldValue().ToRgb(), + change.GetNewValue().ToRgb())); + + ignorePropertyChanged = false; + } + else if (change.Property == PaletteProperty) + { + IColorPalette? palette = Palette; + + // Any custom palette change must be automatically synced with the + // bound properties controlling the palette grid + if (palette != null) + { + PaletteColumnCount = palette.ColorCount; + + List newPaletteColors = new List(); + for (int shadeIndex = 0; shadeIndex < palette.ShadeCount; shadeIndex++) + { + for (int colorIndex = 0; colorIndex < palette.ColorCount; colorIndex++) + { + newPaletteColors.Add(palette.GetColor(colorIndex, shadeIndex)); + } + } + + PaletteColors = newPaletteColors; + } + } + else if (change.Property == IsAlphaEnabledProperty) + { + // Manually coerce the HsvColor value + // (Color will be coerced automatically if HsvColor changes) + HsvColor = OnCoerceHsvColor(HsvColor); + } + else if (change.Property == IsColorComponentsVisibleProperty || + change.Property == IsColorPaletteVisibleProperty || + change.Property == IsColorSpectrumVisibleProperty) + { + // When the property changed notification is received here the visibility + // of individual tab items has not yet been updated through the bindings. + // Therefore, the validation is delayed until after bindings update. + Dispatcher.UIThread.Post(() => + { + ValidateSelection(); + }, DispatcherPriority.Background); + } + else if (change.Property == SelectedIndexProperty) + { + // Again, it is necessary to wait for the SelectedIndex value to + // be applied to the TabControl through binding before validation occurs. + Dispatcher.UIThread.Post(() => + { + ValidateSelection(); + }, DispatcherPriority.Background); + } + + base.OnPropertyChanged(change); + } + + /// + /// Called before the event occurs. + /// + /// The defining old/new colors. + protected virtual void OnColorChanged(ColorChangedEventArgs e) + { + ColorChanged?.Invoke(this, e); + } + + /// + /// Called when the property has to be coerced. + /// + /// The value to coerce. + protected virtual Color OnCoerceColor(Color value) + { + if (IsAlphaEnabled == false) + { + return new Color(255, value.R, value.G, value.B); + } + + return value; + } + + /// + /// Called when the property has to be coerced. + /// + /// The value to coerce. + protected virtual HsvColor OnCoerceHsvColor(HsvColor value) + { + if (IsAlphaEnabled == false) + { + return new HsvColor(1.0, value.H, value.S, value.V); + } + + return value; + } + + /// + /// Coerces/validates the property value. + /// + /// The instance. + /// The value to coerce. + /// The coerced/validated value. + private static Color CoerceColor(IAvaloniaObject instance, Color value) + { + if (instance is ColorView colorView) + { + return colorView.OnCoerceColor(value); + } + + return value; + } + + /// + /// Coerces/validates the property value. + /// + /// The instance. + /// The value to coerce. + /// The coerced/validated value. + private static HsvColor CoerceHsvColor(IAvaloniaObject instance, HsvColor value) + { + if (instance is ColorView colorView) + { + return colorView.OnCoerceHsvColor(value); + } + + return value; + } + + /// + /// Event handler for when a key is pressed within the Hex RGB value TextBox. + /// This is used to trigger re-evaluation of the color based on the TextBox value. + /// + private void HexTextBox_KeyDown(object? sender, Input.KeyEventArgs e) + { + if (e.Key == Input.Key.Enter) + { + GetColorFromHexTextBox(); + } + } + + /// + /// Event handler for when the Hex RGB value TextBox looses focus. + /// This is used to trigger re-evaluation of the color based on the TextBox value. + /// + private void HexTextBox_LostFocus(object? sender, Interactivity.RoutedEventArgs e) + { + GetColorFromHexTextBox(); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs new file mode 100644 index 00000000000..582653e295a --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorViewTab.cs @@ -0,0 +1,26 @@ +namespace Avalonia.Controls +{ + /// + /// Defines a specific tab/page (subview) within the . + /// + /// + /// This is indexed to match the default control template ordering. + /// + public enum ColorViewTab + { + /// + /// The color spectrum subview with a box/ring spectrum and sliders. + /// + Spectrum = 0, + + /// + /// The color palette subview with a grid of selectable colors. + /// + Palette = 1, + + /// + /// The components subview with sliders and numeric input boxes. + /// + Components = 2, + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs index 4d05222e315..2c8e09adc9a 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs @@ -7,8 +7,10 @@ namespace Avalonia.Controls.Primitives.Converters { /// /// Creates an accent color for a given base color value and step parameter. - /// This is a highly-specialized converter for the color picker. /// + /// + /// This is a highly-specialized converter for the color picker. + /// public class AccentColorConverter : IValueConverter { /// diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs index 9b09073d9dc..8d5f2332be2 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs @@ -42,7 +42,7 @@ public class ColorToHexConverter : IValueConverter return AvaloniaProperty.UnsetValue; } - string hexColor = color.ToString(); + string hexColor = color.ToUint32().ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); if (includeSymbol == false) { diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs new file mode 100644 index 00000000000..8b66b1a4e56 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ContrastBrushConverter.cs @@ -0,0 +1,87 @@ +using System; +using System.Globalization; +using Avalonia.Controls.Converters; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives.Converters +{ + /// + /// Gets a , either black or white, depending on the luminance of the supplied color. + /// A default color supplied in the converter parameter may be returned if alpha is below the set threshold. + /// + /// + /// This is a highly-specialized converter for the color picker. + /// + public class ContrastBrushConverter : IValueConverter + { + private ToColorConverter toColorConverter = new ToColorConverter(); + + /// + /// Gets or sets the alpha channel threshold below which a default color is used instead of black/white. + /// + public byte AlphaThreshold { get; set; } = 128; + + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + Color comparisonColor; + Color? defaultColor = null; + + // Get the changing color to compare against + var convertedValue = toColorConverter.Convert(value, targetType, parameter, culture); + if (convertedValue is Color valueColor) + { + comparisonColor = valueColor; + } + else + { + // Invalid color value provided + return AvaloniaProperty.UnsetValue; + } + + // Get the default color when transparency is high + var convertedParameter = toColorConverter.Convert(parameter, targetType, parameter, culture); + if (convertedParameter is Color parameterColor) + { + defaultColor = parameterColor; + } + + if (comparisonColor.A < AlphaThreshold && + defaultColor.HasValue) + { + // If the transparency is less than the threshold, just use the default brush + // This can commonly be something like the TextControlForeground brush + return new SolidColorBrush(defaultColor.Value); + } + else + { + // Chose a white/black brush based on contrast to the base color + if (ColorHelper.GetRelativeLuminance(comparisonColor) <= 0.5) + { + // Dark color, return light for contrast + return new SolidColorBrush(Colors.White); + } + else + { + // Bright color, return dark for contrast + return new SolidColorBrush(Colors.Black); + } + } + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs index 220a993f99d..11e33c74f0d 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs @@ -7,8 +7,10 @@ namespace Avalonia.Controls.Primitives.Converters /// /// Gets the third corresponding with a given /// that represents the other two components. - /// This is a highly-specialized converter for the color picker. /// + /// + /// This is a highly-specialized converter for the color picker. + /// public class ThirdComponentConverter : IValueConverter { /// diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs b/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs deleted file mode 100644 index 2710c220f41..00000000000 --- a/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using Avalonia.Data.Converters; - -namespace Avalonia.Controls.Primitives.Converters -{ - /// - /// Converter to chain together multiple converters. - /// - public class ValueConverterGroup : List, IValueConverter - { - /// - /// - public object? Convert( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - object? curValue; - - curValue = value; - for (int i = 0; i < Count; i++) - { - curValue = this[i].Convert(curValue, targetType, parameter, culture); - } - - return curValue; - } - - /// - public object? ConvertBack( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - object? curValue; - - curValue = value; - for (int i = (Count - 1); i >= 0; i--) - { - curValue = this[i].ConvertBack(curValue, targetType, parameter, culture); - } - - return curValue; - } - } -} diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs index 32a898ee715..7dc340ea16f 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs @@ -64,6 +64,10 @@ public static string ToDisplayName(Color color) // It is also needlessly large as there are only ~140 known/named colors. // Therefore, rounding of the input color's component values is done to // reduce the color space into something more useful. + // + // The rounding value of 5 is specially chosen. + // It is a factor of 255 and therefore evenly divisible which improves + // the quality of the calculations. double rounding = 5; var roundedColor = new Color( 0xFF, diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index 15e5ca16554..c3bc7df4a43 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -1,84 +1,100 @@  - - - - + + 80 + 40 diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml index 19f10201a5a..35cd7a9faae 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml @@ -1,13 +1,7 @@  - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml index cb764a738cb..74f33d1258c 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml @@ -1,84 +1,100 @@  - - - - + + 80 + 40 diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml index 18a081721a5..162ac372de8 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml @@ -1,13 +1,7 @@  - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml index c25d79727fc..186b6de9bc5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml @@ -1,8 +1,10 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:converters="using:Avalonia.Controls.Converters"> - + @@ -18,6 +20,16 @@ + + + + + + + + + + @@ -25,4 +37,8 @@ + + + + diff --git a/src/Avalonia.Controls/Converters/EnumToBoolConverter.cs b/src/Avalonia.Controls/Converters/EnumToBoolConverter.cs new file mode 100644 index 00000000000..ed3065809ee --- /dev/null +++ b/src/Avalonia.Controls/Converters/EnumToBoolConverter.cs @@ -0,0 +1,56 @@ +using System; +using System.Globalization; +using Avalonia.Data; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converter to convert an enum value to bool by comparing to the given parameter. + /// Both value and parameter must be of the same enum type. + /// + /// + /// This converter is useful to enable binding of radio buttons with a selected enum value. + /// + public class EnumToBoolConverter : IValueConverter + { + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + if (value == null && + parameter == null) + { + return true; + } + else if (value == null || + parameter == null) + { + return false; + } + else + { + return value!.Equals(parameter); + } + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + if (value is bool boolValue && + boolValue == true) + { + return parameter; + } + + return BindingOperations.DoNothing; + } + } +} diff --git a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs deleted file mode 100644 index 1a33a82ca44..00000000000 --- a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Data.Converters; - -namespace Avalonia.Controls.Converters -{ - /// - /// Converter that checks if an enum value is equal to the given parameter enum value. - /// - public class EnumValueEqualsConverter : IValueConverter - { - /// - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - // Note: Unlike string comparisons, null/empty is not supported - // Both 'value' and 'parameter' must exist and if both are missing they are not considered equal - if (value != null && - parameter != null) - { - Type type = value.GetType(); - - if (type.IsEnum) - { - var valueStr = value?.ToString(); - var paramStr = parameter?.ToString(); - - if (string.Equals(valueStr, paramStr, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - /* - // TODO: When .net Standard 2.0 is no longer supported the code can be changed to below - // This is a little more type safe - if (type.IsEnum && - Enum.TryParse(type, value?.ToString(), true, out object? valueEnum) && - Enum.TryParse(type, parameter?.ToString(), true, out object? paramEnum)) - { - return valueEnum == paramEnum; - } - */ - } - - return false; - } - - /// - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new System.NotImplementedException(); - } - } -} diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index e50f991cdbe..35033c58f0d 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -291,7 +291,9 @@ public sealed override void ApplyTemplate() // Existing code kinda expect to see a NameScope even if it's empty if (nameScope == null) + { nameScope = new NameScope(); + } var e = new TemplateAppliedEventArgs(nameScope); OnApplyTemplate(e);