diff --git a/Terminal.Gui/Drawing/Color.cs b/Terminal.Gui/Drawing/Color.cs index 8c7b76024..5e0ccf1a8 100644 --- a/Terminal.Gui/Drawing/Color.cs +++ b/Terminal.Gui/Drawing/Color.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Frozen; using System.Diagnostics.Contracts; +using System.Drawing; using System.Globalization; using System.Numerics; using System.Runtime.CompilerServices; @@ -331,4 +332,4 @@ public readonly partial record struct Color : ISpanParsable, IUtf8SpanPar public const ColorName White = ColorName.White; #endregion -} +} \ No newline at end of file diff --git a/Terminal.Gui/Drawing/ColorEventArgs.cs b/Terminal.Gui/Drawing/ColorEventArgs.cs new file mode 100644 index 000000000..66361d503 --- /dev/null +++ b/Terminal.Gui/Drawing/ColorEventArgs.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Terminal.Gui; + +/// Event arguments for the events. +public class ColorEventArgs : EventArgs +{ + /// Initializes a new instance of + /// The value that is being changed to. + public ColorEventArgs (Color newColor) :base(newColor) { } +} \ No newline at end of file diff --git a/Terminal.Gui/Drawing/ColorModel.cs b/Terminal.Gui/Drawing/ColorModel.cs new file mode 100644 index 000000000..e6d523611 --- /dev/null +++ b/Terminal.Gui/Drawing/ColorModel.cs @@ -0,0 +1,25 @@ +#nullable enable + +namespace Terminal.Gui; + +/// +/// Describes away of modelling color e.g. Hue +/// Saturation Lightness. +/// +public enum ColorModel +{ + /// + /// Color modelled by storing Red, Green and Blue as (0-255) ints + /// + RGB, + + /// + /// Color modelled by storing Hue (360 degrees), Saturation (100%) and Value (100%) + /// + HSV, + + /// + /// Color modelled by storing Hue (360 degrees), Saturation (100%) and Lightness (100%) + /// + HSL +} diff --git a/Terminal.Gui/Drawing/ColorStrings.cs b/Terminal.Gui/Drawing/ColorStrings.cs new file mode 100644 index 000000000..2175b56fe --- /dev/null +++ b/Terminal.Gui/Drawing/ColorStrings.cs @@ -0,0 +1,78 @@ +#nullable enable +using System.Collections; +using System.Globalization; +using System.Resources; +using Terminal.Gui.Resources; + +namespace Terminal.Gui; + +/// +/// Provides a mapping between and the W3C standard color name strings. +/// +public static class ColorStrings +{ + private static readonly ResourceManager _resourceManager = new (typeof (Strings)); + + /// + /// Gets the W3C standard string for . + /// + /// The color. + /// if there is no standard color name for the specified color. + public static string? GetW3CColorName (Color color) + { + // Fetch the color name from the resource file + return _resourceManager.GetString ($"#{color.R:X2}{color.G:X2}{color.B:X2}", CultureInfo.CurrentCulture); + } + + /// + /// Returns the list of W3C standard color names. + /// + /// + public static IEnumerable GetW3CColorNames () + { + foreach (DictionaryEntry entry in _resourceManager.GetResourceSet (CultureInfo.CurrentCulture, true, true)!) + { + string keyName = entry.Key.ToString () ?? string.Empty; + + if (entry.Value is string colorName && keyName.StartsWith ('#')) + { + yield return colorName; + } + } + } + + /// + /// Parses and returns if name is a W3C standard named color. + /// + /// The name to parse. + /// If successful, the color. + /// if was parsed successfully. + public static bool TryParseW3CColorName (string name, out Color color) + { + // Iterate through all resource entries to find the matching color name + foreach (DictionaryEntry entry in _resourceManager.GetResourceSet (CultureInfo.CurrentCulture, true, true)!) + { + if (entry.Value is string colorName && colorName.Equals (name, StringComparison.OrdinalIgnoreCase)) + { + // Parse the key to extract the color components + string key = entry.Key.ToString () ?? string.Empty; + + if (key.StartsWith ("#") && key.Length == 7) + { + if (int.TryParse (key.Substring (1, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int r) + && int.TryParse (key.Substring (3, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int g) + && int.TryParse (key.Substring (5, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int b)) + { + color = new (r, g, b); + + return true; + } + } + } + } + + color = default (Color); + + return false; + } +} diff --git a/Terminal.Gui/Drawing/IColorNameResolver.cs b/Terminal.Gui/Drawing/IColorNameResolver.cs new file mode 100644 index 000000000..5af243ee4 --- /dev/null +++ b/Terminal.Gui/Drawing/IColorNameResolver.cs @@ -0,0 +1,34 @@ +namespace Terminal.Gui; + +/// +/// When implemented by a class, allows mapping to +/// human understandable name (e.g. w3c color names) and vice versa. +/// +public interface IColorNameResolver +{ + /// + /// Returns the names of all known colors. + /// + /// + IEnumerable GetColorNames (); + + /// + /// Returns if is a recognized + /// color. In which case will be the name of the color and + /// return value will be true otherwise false. + /// + /// + /// + /// + bool TryNameColor (Color color, out string name); + + /// + /// Returns if is a recognized + /// color. In which case will be the color the name corresponds + /// to otherwise returns false. + /// + /// + /// + /// + bool TryParseColor (string name, out Color color); +} diff --git a/Terminal.Gui/Drawing/W3CColors.cs b/Terminal.Gui/Drawing/W3CColors.cs new file mode 100644 index 000000000..baa1a1101 --- /dev/null +++ b/Terminal.Gui/Drawing/W3CColors.cs @@ -0,0 +1,24 @@ +namespace Terminal.Gui; + +/// +/// Helper class that resolves w3c color names to their hex values +/// Based on https://www.w3schools.com/colors/color_tryit.asp +/// +public class W3CColors : IColorNameResolver +{ + /// + public IEnumerable GetColorNames () { return ColorStrings.GetW3CColorNames (); } + + /// + public bool TryParseColor (string name, out Color color) { return ColorStrings.TryParseW3CColorName (name, out color); } + + /// + public bool TryNameColor (Color color, out string name) + { + string answer = ColorStrings.GetW3CColorName (color); + + name = answer ?? string.Empty; + + return answer != null; + } +} diff --git a/Terminal.Gui/Resources/Strings.Designer.cs b/Terminal.Gui/Resources/Strings.Designer.cs index 99c47bd57..a7bdd6ca9 100644 --- a/Terminal.Gui/Resources/Strings.Designer.cs +++ b/Terminal.Gui/Resources/Strings.Designer.cs @@ -60,6 +60,1257 @@ namespace Terminal.Gui.Resources { } } + /// + /// Looks up a localized string similar to Black. + /// + internal static string _000000 { + get { + return ResourceManager.GetString("#000000", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Navy. + /// + internal static string _000080 { + get { + return ResourceManager.GetString("#000080", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkBlue. + /// + internal static string _00008B { + get { + return ResourceManager.GetString("#00008B", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MediumBlue. + /// + internal static string _0000CD { + get { + return ResourceManager.GetString("#0000CD", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Blue. + /// + internal static string _0000FF { + get { + return ResourceManager.GetString("#0000FF", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkGreen. + /// + internal static string _006400 { + get { + return ResourceManager.GetString("#006400", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Green. + /// + internal static string _008000 { + get { + return ResourceManager.GetString("#008000", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Teal. + /// + internal static string _008080 { + get { + return ResourceManager.GetString("#008080", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkCyan. + /// + internal static string _008B8B { + get { + return ResourceManager.GetString("#008B8B", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DeepSkyBlue. + /// + internal static string _00BFFF { + get { + return ResourceManager.GetString("#00BFFF", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkTurquoise. + /// + internal static string _00CED1 { + get { + return ResourceManager.GetString("#00CED1", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MediumSpringGreen. + /// + internal static string _00FA9A { + get { + return ResourceManager.GetString("#00FA9A", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lime. + /// + internal static string _00FF00 { + get { + return ResourceManager.GetString("#00FF00", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SpringGreen. + /// + internal static string _00FF7F { + get { + return ResourceManager.GetString("#00FF7F", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cyan. + /// + internal static string _00FFFF { + get { + return ResourceManager.GetString("#00FFFF", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MidnightBlue. + /// + internal static string _191970 { + get { + return ResourceManager.GetString("#191970", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DodgerBlue. + /// + internal static string _1E90FF { + get { + return ResourceManager.GetString("#1E90FF", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LightSeaGreen. + /// + internal static string _20B2AA { + get { + return ResourceManager.GetString("#20B2AA", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ForestGreen. + /// + internal static string _228B22 { + get { + return ResourceManager.GetString("#228B22", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SeaGreen. + /// + internal static string _2E8B57 { + get { + return ResourceManager.GetString("#2E8B57", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkSlateGrey. + /// + internal static string _2F4F4F { + get { + return ResourceManager.GetString("#2F4F4F", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LimeGreen. + /// + internal static string _32CD32 { + get { + return ResourceManager.GetString("#32CD32", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MediumSeaGreen. + /// + internal static string _3CB371 { + get { + return ResourceManager.GetString("#3CB371", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Turquoise. + /// + internal static string _40E0D0 { + get { + return ResourceManager.GetString("#40E0D0", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RoyalBlue. + /// + internal static string _4169E1 { + get { + return ResourceManager.GetString("#4169E1", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SteelBlue. + /// + internal static string _4682B4 { + get { + return ResourceManager.GetString("#4682B4", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkSlateBlue. + /// + internal static string _483D8B { + get { + return ResourceManager.GetString("#483D8B", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MediumTurquoise. + /// + internal static string _48D1CC { + get { + return ResourceManager.GetString("#48D1CC", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Indigo. + /// + internal static string _4B0082 { + get { + return ResourceManager.GetString("#4B0082", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkOliveGreen. + /// + internal static string _556B2F { + get { + return ResourceManager.GetString("#556B2F", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CadetBlue. + /// + internal static string _5F9EA0 { + get { + return ResourceManager.GetString("#5F9EA0", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CornflowerBlue. + /// + internal static string _6495ED { + get { + return ResourceManager.GetString("#6495ED", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RebeccaPurple. + /// + internal static string _663399 { + get { + return ResourceManager.GetString("#663399", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MediumAquaMarine. + /// + internal static string _66CDAA { + get { + return ResourceManager.GetString("#66CDAA", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DimGray. + /// + internal static string _696969 { + get { + return ResourceManager.GetString("#696969", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SlateBlue. + /// + internal static string _6A5ACD { + get { + return ResourceManager.GetString("#6A5ACD", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OliveDrab. + /// + internal static string _6B8E23 { + get { + return ResourceManager.GetString("#6B8E23", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SlateGray. + /// + internal static string _708090 { + get { + return ResourceManager.GetString("#708090", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LightSlateGrey. + /// + internal static string _778899 { + get { + return ResourceManager.GetString("#778899", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MediumSlateBlue. + /// + internal static string _7B68EE { + get { + return ResourceManager.GetString("#7B68EE", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LawnGreen. + /// + internal static string _7CFC00 { + get { + return ResourceManager.GetString("#7CFC00", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Chartreuse. + /// + internal static string _7FFF00 { + get { + return ResourceManager.GetString("#7FFF00", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Aquamarine. + /// + internal static string _7FFFD4 { + get { + return ResourceManager.GetString("#7FFFD4", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Maroon. + /// + internal static string _800000 { + get { + return ResourceManager.GetString("#800000", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Purple. + /// + internal static string _800080 { + get { + return ResourceManager.GetString("#800080", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Olive. + /// + internal static string _808000 { + get { + return ResourceManager.GetString("#808000", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gray. + /// + internal static string _808080 { + get { + return ResourceManager.GetString("#808080", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SkyBlue. + /// + internal static string _87CEEB { + get { + return ResourceManager.GetString("#87CEEB", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LightSkyBlue. + /// + internal static string _87CEFA { + get { + return ResourceManager.GetString("#87CEFA", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BlueViolet. + /// + internal static string _8A2BE2 { + get { + return ResourceManager.GetString("#8A2BE2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkRed. + /// + internal static string _8B0000 { + get { + return ResourceManager.GetString("#8B0000", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkMagenta. + /// + internal static string _8B008B { + get { + return ResourceManager.GetString("#8B008B", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SaddleBrown. + /// + internal static string _8B4513 { + get { + return ResourceManager.GetString("#8B4513", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkSeaGreen. + /// + internal static string _8FBC8F { + get { + return ResourceManager.GetString("#8FBC8F", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LightGreen. + /// + internal static string _90EE90 { + get { + return ResourceManager.GetString("#90EE90", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MediumPurple. + /// + internal static string _9370DB { + get { + return ResourceManager.GetString("#9370DB", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkViolet. + /// + internal static string _9400D3 { + get { + return ResourceManager.GetString("#9400D3", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PaleGreen. + /// + internal static string _98FB98 { + get { + return ResourceManager.GetString("#98FB98", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkOrchid. + /// + internal static string _9932CC { + get { + return ResourceManager.GetString("#9932CC", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to YellowGreen. + /// + internal static string _9ACD32 { + get { + return ResourceManager.GetString("#9ACD32", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Sienna. + /// + internal static string _A0522D { + get { + return ResourceManager.GetString("#A0522D", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Brown. + /// + internal static string _A52A2A { + get { + return ResourceManager.GetString("#A52A2A", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkGrey. + /// + internal static string _A9A9A9 { + get { + return ResourceManager.GetString("#A9A9A9", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LightBlue. + /// + internal static string _ADD8E6 { + get { + return ResourceManager.GetString("#ADD8E6", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GreenYellow. + /// + internal static string _ADFF2F { + get { + return ResourceManager.GetString("#ADFF2F", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PaleTurquoise. + /// + internal static string _AFEEEE { + get { + return ResourceManager.GetString("#AFEEEE", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LightSteelBlue. + /// + internal static string _B0C4DE { + get { + return ResourceManager.GetString("#B0C4DE", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PowderBlue. + /// + internal static string _B0E0E6 { + get { + return ResourceManager.GetString("#B0E0E6", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to FireBrick. + /// + internal static string _B22222 { + get { + return ResourceManager.GetString("#B22222", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkGoldenRod. + /// + internal static string _B8860B { + get { + return ResourceManager.GetString("#B8860B", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MediumOrchid. + /// + internal static string _BA55D3 { + get { + return ResourceManager.GetString("#BA55D3", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RosyBrown. + /// + internal static string _BC8F8F { + get { + return ResourceManager.GetString("#BC8F8F", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkKhaki. + /// + internal static string _BDB76B { + get { + return ResourceManager.GetString("#BDB76B", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Silver. + /// + internal static string _C0C0C0 { + get { + return ResourceManager.GetString("#C0C0C0", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MediumVioletRed. + /// + internal static string _C71585 { + get { + return ResourceManager.GetString("#C71585", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to IndianRed. + /// + internal static string _CD5C5C { + get { + return ResourceManager.GetString("#CD5C5C", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Peru. + /// + internal static string _CD853F { + get { + return ResourceManager.GetString("#CD853F", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Chocolate. + /// + internal static string _D2691E { + get { + return ResourceManager.GetString("#D2691E", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tan. + /// + internal static string _D2B48C { + get { + return ResourceManager.GetString("#D2B48C", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LightGray. + /// + internal static string _D3D3D3 { + get { + return ResourceManager.GetString("#D3D3D3", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Thistle. + /// + internal static string _D8BFD8 { + get { + return ResourceManager.GetString("#D8BFD8", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Orchid. + /// + internal static string _DA70D6 { + get { + return ResourceManager.GetString("#DA70D6", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GoldenRod. + /// + internal static string _DAA520 { + get { + return ResourceManager.GetString("#DAA520", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PaleVioletRed. + /// + internal static string _DB7093 { + get { + return ResourceManager.GetString("#DB7093", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Crimson. + /// + internal static string _DC143C { + get { + return ResourceManager.GetString("#DC143C", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gainsboro. + /// + internal static string _DCDCDC { + get { + return ResourceManager.GetString("#DCDCDC", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Plum. + /// + internal static string _DDA0DD { + get { + return ResourceManager.GetString("#DDA0DD", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BurlyWood. + /// + internal static string _DEB887 { + get { + return ResourceManager.GetString("#DEB887", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LightCyan. + /// + internal static string _E0FFFF { + get { + return ResourceManager.GetString("#E0FFFF", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lavender. + /// + internal static string _E6E6FA { + get { + return ResourceManager.GetString("#E6E6FA", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkSalmon. + /// + internal static string _E9967A { + get { + return ResourceManager.GetString("#E9967A", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Violet. + /// + internal static string _EE82EE { + get { + return ResourceManager.GetString("#EE82EE", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PaleGoldenRod. + /// + internal static string _EEE8AA { + get { + return ResourceManager.GetString("#EEE8AA", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LightCoral. + /// + internal static string _F08080 { + get { + return ResourceManager.GetString("#F08080", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Khaki. + /// + internal static string _F0E68C { + get { + return ResourceManager.GetString("#F0E68C", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to AliceBlue. + /// + internal static string _F0F8FF { + get { + return ResourceManager.GetString("#F0F8FF", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to HoneyDew. + /// + internal static string _F0FFF0 { + get { + return ResourceManager.GetString("#F0FFF0", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Azure. + /// + internal static string _F0FFFF { + get { + return ResourceManager.GetString("#F0FFFF", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SandyBrown. + /// + internal static string _F4A460 { + get { + return ResourceManager.GetString("#F4A460", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wheat. + /// + internal static string _F5DEB3 { + get { + return ResourceManager.GetString("#F5DEB3", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Beige. + /// + internal static string _F5F5DC { + get { + return ResourceManager.GetString("#F5F5DC", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to WhiteSmoke. + /// + internal static string _F5F5F5 { + get { + return ResourceManager.GetString("#F5F5F5", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MintCream. + /// + internal static string _F5FFFA { + get { + return ResourceManager.GetString("#F5FFFA", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GhostWhite. + /// + internal static string _F8F8FF { + get { + return ResourceManager.GetString("#F8F8FF", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Salmon. + /// + internal static string _FA8072 { + get { + return ResourceManager.GetString("#FA8072", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to AntiqueWhite. + /// + internal static string _FAEBD7 { + get { + return ResourceManager.GetString("#FAEBD7", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Linen. + /// + internal static string _FAF0E6 { + get { + return ResourceManager.GetString("#FAF0E6", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LightGoldenRodYellow. + /// + internal static string _FAFAD2 { + get { + return ResourceManager.GetString("#FAFAD2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OldLace. + /// + internal static string _FDF5E6 { + get { + return ResourceManager.GetString("#FDF5E6", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Red. + /// + internal static string _FF0000 { + get { + return ResourceManager.GetString("#FF0000", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Magenta. + /// + internal static string _FF00FF { + get { + return ResourceManager.GetString("#FF00FF", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DeepPink. + /// + internal static string _FF1493 { + get { + return ResourceManager.GetString("#FF1493", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OrangeRed. + /// + internal static string _FF4500 { + get { + return ResourceManager.GetString("#FF4500", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tomato. + /// + internal static string _FF6347 { + get { + return ResourceManager.GetString("#FF6347", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to HotPink. + /// + internal static string _FF69B4 { + get { + return ResourceManager.GetString("#FF69B4", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Coral. + /// + internal static string _FF7F50 { + get { + return ResourceManager.GetString("#FF7F50", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DarkOrange. + /// + internal static string _FF8C00 { + get { + return ResourceManager.GetString("#FF8C00", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LightSalmon. + /// + internal static string _FFA07A { + get { + return ResourceManager.GetString("#FFA07A", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Orange. + /// + internal static string _FFA500 { + get { + return ResourceManager.GetString("#FFA500", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LightPink. + /// + internal static string _FFB6C1 { + get { + return ResourceManager.GetString("#FFB6C1", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pink. + /// + internal static string _FFC0CB { + get { + return ResourceManager.GetString("#FFC0CB", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gold. + /// + internal static string _FFD700 { + get { + return ResourceManager.GetString("#FFD700", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PeachPuff. + /// + internal static string _FFDAB9 { + get { + return ResourceManager.GetString("#FFDAB9", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to NavajoWhite. + /// + internal static string _FFDEAD { + get { + return ResourceManager.GetString("#FFDEAD", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Moccasin. + /// + internal static string _FFE4B5 { + get { + return ResourceManager.GetString("#FFE4B5", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bisque. + /// + internal static string _FFE4C4 { + get { + return ResourceManager.GetString("#FFE4C4", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MistyRose. + /// + internal static string _FFE4E1 { + get { + return ResourceManager.GetString("#FFE4E1", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BlanchedAlmond. + /// + internal static string _FFEBCD { + get { + return ResourceManager.GetString("#FFEBCD", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PapayaWhip. + /// + internal static string _FFEFD5 { + get { + return ResourceManager.GetString("#FFEFD5", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LavenderBlush. + /// + internal static string _FFF0F5 { + get { + return ResourceManager.GetString("#FFF0F5", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SeaShell. + /// + internal static string _FFF5EE { + get { + return ResourceManager.GetString("#FFF5EE", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cornsilk. + /// + internal static string _FFF8DC { + get { + return ResourceManager.GetString("#FFF8DC", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LemonChiffon. + /// + internal static string _FFFACD { + get { + return ResourceManager.GetString("#FFFACD", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to FloralWhite. + /// + internal static string _FFFAF0 { + get { + return ResourceManager.GetString("#FFFAF0", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Snow. + /// + internal static string _FFFAFA { + get { + return ResourceManager.GetString("#FFFAFA", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Yellow. + /// + internal static string _FFFF00 { + get { + return ResourceManager.GetString("#FFFF00", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to LightYellow. + /// + internal static string _FFFFE0 { + get { + return ResourceManager.GetString("#FFFFE0", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ivory. + /// + internal static string _FFFFF0 { + get { + return ResourceManager.GetString("#FFFFF0", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to White. + /// + internal static string _FFFFFF { + get { + return ResourceManager.GetString("#FFFFFF", resourceCulture); + } + } + /// /// Looks up a localized string similar to _Cancel. /// diff --git a/Terminal.Gui/Resources/Strings.resx b/Terminal.Gui/Resources/Strings.resx index 60fbac4f7..1ce166e14 100644 --- a/Terminal.Gui/Resources/Strings.resx +++ b/Terminal.Gui/Resources/Strings.resx @@ -1,6 +1,6 @@  - - - - - - - + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + _Select All - + _Delete All - + _Copy - + Cu_t - + _Paste - + _Undo - + _Redo - + Directory - + File - + Save - + Save as - + Open - + Select folder - + Select Mixed - + _Back - + Fi_nish - + _Next... - + Directory already exists with that name When trying to save a file with a name already taken by a directory - + Must select an existing directory - + File already exists with that name - + Must select an existing file When trying to save a directory with a name already used by a file - + Filename - + Must select an existing file or directory - + Modified - + Enter Path - + Enter Search - + Size - + Type - + Wrong file type When trying to open/save a file that does not match the provided filter (e.g. csv) - + Any Files Describes an AllowedType that matches anything - + Are you sure you want to delete '{0}'? This operation is permanent - + Delete Failed - + Delete {0} - + New Failed - + New Folder - + _No - + Rename Failed - + Name: - + Rename - + _Yes - + Existing - + O_pen - + _Save - + Save _as - + _OK - + _Cancel - + _Delete - + _Hide {0} - + _New - + _Rename - + _Sort {0} ASC - + _Sort {0} DESC - + Date Picker + + AliceBlue + + + AntiqueWhite + + + Aquamarine + + + Azure + + + Beige + + + Bisque + + + Black + + + BlanchedAlmond + + + Blue + + + BlueViolet + + + Brown + + + BurlyWood + + + CadetBlue + + + Chartreuse + + + Chocolate + + + Coral + + + CornflowerBlue + + + Cornsilk + + + Crimson + + + Cyan + + + DarkBlue + + + DarkCyan + + + DarkGoldenRod + + + DarkGrey + + + DarkGreen + + + DarkKhaki + + + DarkMagenta + + + DarkOliveGreen + + + DarkOrange + + + DarkOrchid + + + DarkRed + + + DarkSalmon + + + DarkSeaGreen + + + DarkSlateBlue + + + DarkSlateGrey + + + DarkTurquoise + + + DarkViolet + + + DeepPink + + + DeepSkyBlue + + + DimGray + + + DodgerBlue + + + FireBrick + + + FloralWhite + + + ForestGreen + + + Gainsboro + + + GhostWhite + + + Gold + + + GoldenRod + + + Gray + + + Green + + + GreenYellow + + + HoneyDew + + + HotPink + + + IndianRed + + + Indigo + + + Ivory + + + Khaki + + + Lavender + + + LavenderBlush + + + LawnGreen + + + LemonChiffon + + + LightBlue + + + LightCoral + + + LightCyan + + + LightGoldenRodYellow + + + LightGray + + + LightGreen + + + LightPink + + + LightSalmon + + + LightSeaGreen + + + LightSkyBlue + + + LightSlateGrey + + + LightSteelBlue + + + LightYellow + + + Lime + + + LimeGreen + + + Linen + + + Magenta + + + Maroon + + + MediumAquaMarine + + + MediumBlue + + + MediumOrchid + + + MediumPurple + + + MediumSeaGreen + + + MediumSlateBlue + + + MediumSpringGreen + + + MediumTurquoise + + + MediumVioletRed + + + MidnightBlue + + + MintCream + + + MistyRose + + + Moccasin + + + NavajoWhite + + + Navy + + + OldLace + + + Olive + + + OliveDrab + + + Orange + + + OrangeRed + + + Orchid + + + PaleGoldenRod + + + PaleGreen + + + PaleTurquoise + + + PaleVioletRed + + + PapayaWhip + + + PeachPuff + + + Peru + + + Pink + + + Plum + + + PowderBlue + + + Purple + + + RebeccaPurple + + + Red + + + RosyBrown + + + RoyalBlue + + + SaddleBrown + + + Salmon + + + SandyBrown + + + SeaGreen + + + SeaShell + + + Sienna + + + Silver + + + SkyBlue + + + SlateBlue + + + SlateGray + + + Snow + + + SpringGreen + + + SteelBlue + + + Tan + + + Teal + + + Thistle + + + Tomato + + + Turquoise + + + Violet + + + Wheat + + + White + + + WhiteSmoke + + + Yellow + + + YellowGreen + \ No newline at end of file diff --git a/Terminal.Gui/Views/BBar.cs b/Terminal.Gui/Views/BBar.cs new file mode 100644 index 000000000..c42dfdd9a --- /dev/null +++ b/Terminal.Gui/Views/BBar.cs @@ -0,0 +1,27 @@ +#nullable enable + +using ColorHelper; + +namespace Terminal.Gui; + +internal class BBar : ColorBar +{ + public GBar? GBar { get; set; } + public RBar? RBar { get; set; } + + /// + protected override Color GetColor (double fraction) + { + if (RBar == null || GBar == null) + { + throw new ($"{nameof (BBar)} has not been set up correctly before drawing"); + } + + var rgb = new RGB ((byte)RBar.Value, (byte)GBar.Value, (byte)(MaxValue * fraction)); + + return new (rgb.R, rgb.G, rgb.B); + } + + /// + protected override int MaxValue => 255; +} diff --git a/Terminal.Gui/Views/ColorBar.cs b/Terminal.Gui/Views/ColorBar.cs new file mode 100644 index 000000000..8ca0e4554 --- /dev/null +++ b/Terminal.Gui/Views/ColorBar.cs @@ -0,0 +1,235 @@ +#nullable enable + +namespace Terminal.Gui; + +/// +/// A bar representing a single component of a e.g. +/// the Red portion of a . +/// +internal abstract class ColorBar : View, IColorBar +{ + /// + /// Creates a new instance of the class. + /// + protected ColorBar () + { + Height = 1; + Width = Dim.Fill (); + CanFocus = true; + + AddCommand (Command.Left, _ => Adjust (-1)); + AddCommand (Command.Right, _ => Adjust (1)); + + AddCommand (Command.LeftExtend, _ => Adjust (-MaxValue / 20)); + AddCommand (Command.RightExtend, _ => Adjust (MaxValue / 20)); + + AddCommand (Command.LeftHome, _ => SetZero ()); + AddCommand (Command.RightEnd, _ => SetMax ()); + + KeyBindings.Add (Key.CursorLeft, Command.Left); + KeyBindings.Add (Key.CursorRight, Command.Right); + KeyBindings.Add (Key.CursorLeft.WithShift, Command.LeftExtend); + KeyBindings.Add (Key.CursorRight.WithShift, Command.RightExtend); + KeyBindings.Add (Key.Home, Command.LeftHome); + KeyBindings.Add (Key.End, Command.RightEnd); + } + + /// + /// X coordinate that the bar starts at excluding any label. + /// + private int _barStartsAt; + + /// + /// 0-1 for how much of the color element is present currently (HSL) + /// + private int _value; + + /// + /// The amount of represented by each cell width on the bar + /// Can be less than 1 e.g. if Saturation (0-100) and width > 100 + /// + private double _cellValue = 1d; + + /// + /// Last known width of the bar as passed to . + /// + private int _barWidth; + + /// + /// The currently selected amount of the color component stored by this class e.g. + /// the amount of Hue in a . + /// + public int Value + { + get => _value; + set + { + int clampedValue = Math.Clamp (value, 0, MaxValue); + + if (_value != clampedValue) + { + _value = clampedValue; + OnValueChanged (); + } + } + } + + /// + void IColorBar.SetValueWithoutRaisingEvent (int v) + { + _value = v; + SetNeedsDisplay (); + } + + /// + public override void OnDrawContent (Rectangle viewport) + { + base.OnDrawContent (viewport); + + var xOffset = 0; + + if (!string.IsNullOrWhiteSpace (Text)) + { + Move (0, 0); + Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ()); + Driver.AddStr (Text); + + // TODO: is there a better method than this? this is what it is in TableView + xOffset = Text.EnumerateRunes ().Sum (c => c.GetColumns ()); + } + + _barWidth = viewport.Width - xOffset; + _barStartsAt = xOffset; + + DrawBar (xOffset, 0, _barWidth); + } + + /// + /// Event fired when is changed to a new value + /// + public event EventHandler>? ValueChanged; + + /// + protected internal override bool OnMouseEvent (MouseEvent mouseEvent) + { + if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) + { + if (mouseEvent.Position.X >= _barStartsAt) + { + double v = MaxValue * ((double)mouseEvent.Position.X - _barStartsAt) / (_barWidth - 1); + Value = Math.Clamp ((int)v, 0, MaxValue); + } + + mouseEvent.Handled = true; + FocusFirst (null); + + return true; + } + + return base.OnMouseEvent (mouseEvent); + } + + /// + /// When overriden in a derived class, returns the to + /// render at proportion of the full bars width. + /// e.g. 0.5 fraction of Saturation is 50% because Saturation goes from 0-100. + /// + /// + /// + protected abstract Color GetColor (double fraction); + + /// + /// The maximum value allowed for this component e.g. Saturation allows up to 100 as it + /// is a percentage while Hue allows up to 360 as it is measured in degrees. + /// + protected abstract int MaxValue { get; } + + /// + /// The last drawn location in View's viewport where the Triangle appeared. + /// Used exclusively for tests. + /// + internal int TrianglePosition { get; private set; } + + private bool? Adjust (int delta) + { + var change = (int)(delta * _cellValue); + + // Ensure that the change is at least 1 or -1 if delta is non-zero + if (change == 0 && delta != 0) + { + change = delta > 0 ? 1 : -1; + } + + Value += change; + + return true; + } + + private void DrawBar (int xOffset, int yOffset, int width) + { + // Each 1 unit of X in the bar corresponds to this much of Value + _cellValue = (double)MaxValue / (width - 1); + + for (var x = 0; x < width; x++) + { + double fraction = (double)x / (width - 1); + Color color = GetColor (fraction); + + // Adjusted isSelectedCell calculation + double cellBottomThreshold = (x - 1) * _cellValue; + double cellTopThreshold = x * _cellValue; + + if (x == width - 1) + { + cellTopThreshold = MaxValue; + } + + bool isSelectedCell = Value > cellBottomThreshold && Value <= cellTopThreshold; + + // Check the brightness of the background color + double brightness = (0.299 * color.R + 0.587 * color.G + 0.114 * color.B) / 255; + + Color triangleColor = Color.Black; + + if (brightness < 0.15) // Threshold to determine if the color is too close to black + { + triangleColor = Color.DarkGray; + } + + if (isSelectedCell) + { + // Draw the triangle at the closest position + Application.Driver?.SetAttribute (new (triangleColor, color)); + AddRune (x + xOffset, yOffset, new ('▲')); + + // Record for tests + TrianglePosition = x + xOffset; + } + else + { + Application.Driver?.SetAttribute (new (color, color)); + AddRune (x + xOffset, yOffset, new ('█')); + } + } + } + + private void OnValueChanged () + { + ValueChanged?.Invoke (this, new (in _value)); + SetNeedsDisplay (); + } + + private bool? SetMax () + { + Value = MaxValue; + + return true; + } + + private bool? SetZero () + { + Value = 0; + + return true; + } +} diff --git a/Terminal.Gui/Views/ColorModelStrategy.cs b/Terminal.Gui/Views/ColorModelStrategy.cs new file mode 100644 index 000000000..50a958296 --- /dev/null +++ b/Terminal.Gui/Views/ColorModelStrategy.cs @@ -0,0 +1,163 @@ +#nullable enable + +using ColorHelper; +using ColorConverter = ColorHelper.ColorConverter; + +namespace Terminal.Gui; + +internal class ColorModelStrategy +{ + public IEnumerable CreateBars (ColorModel model) + { + switch (model) + { + case ColorModel.RGB: + return CreateRgbBars (); + case ColorModel.HSV: + return CreateHsvBars (); + case ColorModel.HSL: + return CreateHslBars (); + default: + throw new ArgumentOutOfRangeException (nameof (model), model, null); + } + } + + public Color GetColorFromBars (IList bars, ColorModel model) + { + switch (model) + { + case ColorModel.RGB: + return ToColor (new ((byte)bars [0].Value, (byte)bars [1].Value, (byte)bars [2].Value)); + case ColorModel.HSV: + return ToColor ( + ColorConverter.HsvToRgb (new (bars [0].Value, (byte)bars [1].Value, (byte)bars [2].Value)) + ); + case ColorModel.HSL: + return ToColor ( + ColorConverter.HslToRgb (new (bars [0].Value, (byte)bars [1].Value, (byte)bars [2].Value)) + ); + default: + throw new ArgumentOutOfRangeException (nameof (model), model, null); + } + } + + public void SetBarsToColor (IList bars, Color newValue, ColorModel model) + { + switch (model) + { + case ColorModel.RGB: + bars [0].SetValueWithoutRaisingEvent (newValue.R); + bars [1].SetValueWithoutRaisingEvent (newValue.G); + bars [2].SetValueWithoutRaisingEvent (newValue.B); + + break; + case ColorModel.HSV: + HSV newHsv = ColorConverter.RgbToHsv (new (newValue.R, newValue.G, newValue.B)); + bars [0].SetValueWithoutRaisingEvent (newHsv.H); + bars [1].SetValueWithoutRaisingEvent (newHsv.S); + bars [2].SetValueWithoutRaisingEvent (newHsv.V); + + break; + case ColorModel.HSL: + + HSL newHsl = ColorConverter.RgbToHsl (new (newValue.R, newValue.G, newValue.B)); + bars [0].SetValueWithoutRaisingEvent (newHsl.H); + bars [1].SetValueWithoutRaisingEvent (newHsl.S); + bars [2].SetValueWithoutRaisingEvent (newHsl.L); + + break; + default: + throw new ArgumentOutOfRangeException (nameof (model), model, null); + } + } + + private IEnumerable CreateHslBars () + { + var h = new HueBar + { + Text = "H:" + }; + + yield return h; + + var s = new SaturationBar + { + Text = "S:" + }; + + var l = new LightnessBar + { + Text = "L:" + }; + + s.HBar = h; + s.LBar = l; + + l.HBar = h; + l.SBar = s; + + yield return s; + yield return l; + } + + private IEnumerable CreateHsvBars () + { + var h = new HueBar + { + Text = "H:" + }; + + yield return h; + + var s = new SaturationBar + { + Text = "S:" + }; + + var v = new ValueBar + { + Text = "V:" + }; + + s.HBar = h; + s.VBar = v; + + v.HBar = h; + v.SBar = s; + + yield return s; + yield return v; + } + + private IEnumerable CreateRgbBars () + { + var r = new RBar + { + Text = "R:" + }; + + var g = new GBar + { + Text = "G:" + }; + + var b = new BBar + { + Text = "B:" + }; + r.GBar = g; + r.BBar = b; + + g.RBar = r; + g.BBar = b; + + b.RBar = r; + b.GBar = g; + + yield return r; + yield return g; + yield return b; + } + + private Color ToColor (RGB rgb) { return new (rgb.R, rgb.G, rgb.B); } +} diff --git a/Terminal.Gui/Views/ColorPicker.cs b/Terminal.Gui/Views/ColorPicker.cs index 6ad6dc305..5865e7307 100644 --- a/Terminal.Gui/Views/ColorPicker.cs +++ b/Terminal.Gui/Views/ColorPicker.cs @@ -1,279 +1,318 @@ -namespace Terminal.Gui; +#nullable enable -/// Event arguments for the events. -public class ColorEventArgs : EventArgs -{ - /// Initializes a new instance of - public ColorEventArgs () { } +namespace Terminal.Gui; - /// The new Thickness. - public Color Color { get; set; } - - /// The previous Thickness. - public Color PreviousColor { get; set; } -} - -/// The Color picker. +/// +/// True color picker using HSL +/// public class ColorPicker : View { - /// Columns of color boxes - private readonly int _cols = 8; - - /// Rows of color boxes - private readonly int _rows = 2; - - private int _boxHeight = 2; - private int _boxWidth = 4; - private int _selectColorIndex = (int)Color.Black; - - /// Initializes a new instance of . - public ColorPicker () { SetInitialProperties (); } - - private void SetInitialProperties () + /// + /// Creates a new instance of . Use + /// to change color model. Use + /// to change initial . + /// + public ColorPicker () { - HighlightStyle = Gui.HighlightStyle.PressedOutside | Gui.HighlightStyle.Pressed; - CanFocus = true; - AddCommands (); - AddKeyBindings (); - - Width = Dim.Auto (minimumContentDim: _boxWidth * _cols); - Height = Dim.Auto (minimumContentDim: _boxHeight * _rows); - SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows)); - - MouseClick += ColorPicker_MouseClick; + Height = Dim.Auto (); + Width = Dim.Auto (); + ApplyStyleChanges (); } - // TODO: Decouple Cursor from SelectedColor so that mouse press-and-hold can show the color under the cursor. + private readonly Dictionary _textFields = new (); + private readonly ColorModelStrategy _strategy = new (); + private TextField? _tfHex; + private Label? _lbHex; - private void ColorPicker_MouseClick (object sender, MouseEventEventArgs me) - { - // if (CanFocus) - { - Cursor = new Point (me.MouseEvent.Position.X / _boxWidth, me.MouseEvent.Position.Y / _boxHeight); - SetFocus (); - me.Handled = true; - } - } + private TextField? _tfName; + private Label? _lbName; - /// Height of a color box - public int BoxHeight + private Color _selectedColor = Color.Black; + + // TODO: Add interface + private readonly IColorNameResolver _colorNameResolver = new W3CColors (); + + private List _bars = new (); + + /// + /// Rebuild the user interface to reflect the new state of . + /// + public void ApplyStyleChanges () { - get => _boxHeight; - set + Color oldValue = _selectedColor; + DisposeOldViews (); + + var y = 0; + const int textFieldWidth = 4; + + foreach (ColorBar bar in _strategy.CreateBars (Style.ColorModel)) { - if (_boxHeight != value) + bar.Y = y; + bar.Width = Dim.Fill (Style.ShowTextFields ? textFieldWidth : 0); + + if (Style.ShowTextFields) { - _boxHeight = value; - Width = Dim.Auto (minimumContentDim: _boxWidth * _cols); - Height = Dim.Auto (minimumContentDim: _boxHeight * _rows); - SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows)); - SetNeedsLayout (); + var tfValue = new TextField + { + X = Pos.AnchorEnd (textFieldWidth), + Y = y, + Width = textFieldWidth + }; + tfValue.Leave += UpdateSingleBarValueFromTextField; + _textFields.Add (bar, tfValue); + Add (tfValue); } - } - } - /// Width of a color box - public int BoxWidth - { - get => _boxWidth; - set + y++; + + bar.ValueChanged += RebuildColorFromBar; + + _bars.Add (bar); + + Add (bar); + } + + if (Style.ShowColorName) { - if (_boxWidth != value) - { - _boxWidth = value; - Width = Dim.Auto (minimumContentDim: _boxWidth * _cols); - Height = Dim.Auto (minimumContentDim: _boxHeight * _rows); - SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows)); - SetNeedsLayout (); - } - } - } - - /// Cursor for the selected color. - public Point Cursor - { - get => new (_selectColorIndex % _cols, _selectColorIndex / _cols); - set - { - int colorIndex = value.Y * _cols + value.X; - SelectedColor = (ColorName)colorIndex; - } - } - - /// Selected color. - public ColorName SelectedColor - { - get => (ColorName)_selectColorIndex; - set - { - if (value == (ColorName)_selectColorIndex) - { - return; - } - var prev = (ColorName)_selectColorIndex; - _selectColorIndex = (int)value; - - ColorChanged?.Invoke ( - this, - new ColorEventArgs { PreviousColor = new Color (prev), Color = new Color (value) } - ); - SetNeedsDisplay (); - } - } - - /// Fired when a color is picked. - public event EventHandler ColorChanged; - - - /// Moves the selected item index to the previous column. - /// - public virtual bool MoveLeft () - { - if (Cursor.X > 0) - { - SelectedColor--; + CreateNameField (); } - return true; + CreateTextField (); + SelectedColor = oldValue; + + LayoutSubviews (); } - /// Moves the selected item index to the next column. - /// - public virtual bool MoveRight () - { - if (Cursor.X < _cols - 1) - { - SelectedColor++; - } + /// + /// Fired when color is changed. + /// + public event EventHandler? ColorChanged; - return true; - } - - /// Moves the selected item index to the previous row. - /// - public virtual bool MoveUp () - { - if (Cursor.Y > 0) - { - SelectedColor -= _cols; - } - - return true; - } - - /// Moves the selected item index to the next row. - /// - public virtual bool MoveDown () - { - if (Cursor.Y < _rows - 1) - { - SelectedColor += _cols; - } - - return true; - } - - /// + /// public override void OnDrawContent (Rectangle viewport) { base.OnDrawContent (viewport); + Attribute normal = GetNormalColor (); + Driver.SetAttribute (new (SelectedColor, normal.Background)); + int y = _bars.Count + (Style.ShowColorName ? 1 : 0); + AddRune (13, y, (Rune)'■'); + } - Driver.SetAttribute (HasFocus ? ColorScheme.Focus : GetNormalColor ()); - var colorIndex = 0; + /// + /// The color selected in the picker + /// + public Color SelectedColor + { + get => _selectedColor; + set => SetSelectedColor (value, true); + } - for (var y = 0; y < Math.Max (2, viewport.Height / BoxHeight); y++) + /// + /// Style settings for the color picker. After making changes ensure you call + /// . + /// + public ColorPickerStyle Style { get; set; } = new (); + + private void CreateNameField () + { + _lbName = new () { - for (var x = 0; x < Math.Max (8, viewport.Width / BoxWidth); x++) + Text = "Name:", + X = 0, + Y = 3 + }; + + _tfName = new () + { + Y = 3, + X = 6, + Width = 20 // width of "LightGoldenRodYellow" - the longest w3c color name + }; + + Add (_lbName); + Add (_tfName); + + var auto = new AppendAutocomplete (_tfName); + + auto.SuggestionGenerator = new SingleWordSuggestionGenerator + { + AllSuggestions = _colorNameResolver.GetColorNames ().ToList () + }; + _tfName.Autocomplete = auto; + + _tfName.Leave += UpdateValueFromName; + } + + private void CreateTextField () + { + int y = _bars.Count; + + if (Style.ShowColorName) + { + y++; + } + + _lbHex = new () + { + Text = "Hex:", + X = 0, + Y = y + }; + + _tfHex = new () + { + Y = y, + X = 4, + Width = 8 + }; + + Add (_lbHex); + Add (_tfHex); + + _tfHex.Leave += UpdateValueFromTextField; + } + + private void DisposeOldViews () + { + foreach (ColorBar bar in _bars.Cast ()) + { + bar.ValueChanged -= RebuildColorFromBar; + + if (_textFields.TryGetValue (bar, out TextField? tf)) { - int foregroundColorIndex = y == 0 ? colorIndex + _cols : colorIndex - _cols; - Driver.SetAttribute (new Attribute ((ColorName)foregroundColorIndex, (ColorName)colorIndex)); - bool selected = x == Cursor.X && y == Cursor.Y; - DrawColorBox (x, y, selected); - colorIndex++; + tf.Leave -= UpdateSingleBarValueFromTextField; + Remove (tf); + tf.Dispose (); + } + + Remove (bar); + bar.Dispose (); + } + + _bars = new (); + _textFields.Clear (); + + if (_lbHex != null) + { + Remove (_lbHex); + _lbHex.Dispose (); + _lbHex = null; + } + + if (_tfHex != null) + { + Remove (_tfHex); + _tfHex.Leave -= UpdateValueFromTextField; + _tfHex.Dispose (); + _tfHex = null; + } + + if (_lbName != null) + { + Remove (_lbName); + _lbName.Dispose (); + _lbName = null; + } + + if (_tfName != null) + { + Remove (_tfName); + _tfName.Leave -= UpdateValueFromName; + _tfName.Dispose (); + _tfName = null; + } + } + + private void RebuildColorFromBar (object? sender, EventArgs e) { SetSelectedColor (_strategy.GetColorFromBars (_bars, Style.ColorModel), false); } + + private void SetSelectedColor (Color value, bool syncBars) + { + if (_selectedColor != value) + { + Color old = _selectedColor; + _selectedColor = value; + + ColorChanged?.Invoke ( + this, + new (value)); + } + + SyncSubViewValues (syncBars); + } + + private void SyncSubViewValues (bool syncBars) + { + if (syncBars) + { + _strategy.SetBarsToColor (_bars, _selectedColor, Style.ColorModel); + } + + foreach (KeyValuePair kvp in _textFields) + { + kvp.Value.Text = kvp.Key.Value.ToString (); + } + + var colorHex = _selectedColor.ToString ($"#{SelectedColor.R:X2}{SelectedColor.G:X2}{SelectedColor.B:X2}"); + + if (_tfName != null) + { + _tfName.Text = _colorNameResolver.TryNameColor (_selectedColor, out string name) ? name : string.Empty; + } + + if (_tfHex != null) + { + _tfHex.Text = colorHex; + } + } + + private void UpdateSingleBarValueFromTextField (object? sender, FocusEventArgs e) + { + foreach (KeyValuePair kvp in _textFields) + { + if (kvp.Value == sender) + { + if (int.TryParse (kvp.Value.Text, out int v)) + { + kvp.Key.Value = v; + } } } } - - /// Add the commands. - private void AddCommands () + private void UpdateValueFromName (object? sender, FocusEventArgs e) { - AddCommand (Command.Left, () => MoveLeft ()); - AddCommand (Command.Right, () => MoveRight ()); - AddCommand (Command.LineUp, () => MoveUp ()); - AddCommand (Command.LineDown, () => MoveDown ()); - } - - /// Add the KeyBindinds. - private void AddKeyBindings () - { - KeyBindings.Add (Key.CursorLeft, Command.Left); - KeyBindings.Add (Key.CursorRight, Command.Right); - KeyBindings.Add (Key.CursorUp, Command.LineUp); - KeyBindings.Add (Key.CursorDown, Command.LineDown); - } - - /// Draw a box for one color. - /// X location. - /// Y location - /// - private void DrawColorBox (int x, int y, bool selected) - { - var index = 0; - - for (var zoomedY = 0; zoomedY < BoxHeight; zoomedY++) + if (_tfName == null) { - for (var zoomedX = 0; zoomedX < BoxWidth; zoomedX++) - { - Move (x * BoxWidth + zoomedX, y * BoxHeight + zoomedY); - Driver.AddRune ((Rune)' '); - index++; - } + return; } - if (selected) + if (_colorNameResolver.TryParseColor (_tfName.Text, out Color newColor)) { - DrawFocusRect (new (x * BoxWidth, y * BoxHeight, BoxWidth, BoxHeight)); - } - } - - private void DrawFocusRect (Rectangle rect) - { - var lc = new LineCanvas (); - - if (rect.Width == 1) - { - lc.AddLine (rect.Location, rect.Height, Orientation.Vertical, LineStyle.Dotted); - } - else if (rect.Height == 1) - { - lc.AddLine (rect.Location, rect.Width, Orientation.Horizontal, LineStyle.Dotted); + SelectedColor = newColor; } else { - lc.AddLine (rect.Location, rect.Width, Orientation.Horizontal, LineStyle.Dotted); + // value is invalid, revert the value in the text field back to current state + SyncSubViewValues (false); + } + } - lc.AddLine ( - rect.Location with { Y = rect.Location.Y + rect.Height - 1 }, - rect.Width, - Orientation.Horizontal, - LineStyle.Dotted - ); - - lc.AddLine (rect.Location, rect.Height, Orientation.Vertical, LineStyle.Dotted); - - lc.AddLine ( - rect.Location with { X = rect.Location.X + rect.Width - 1 }, - rect.Height, - Orientation.Vertical, - LineStyle.Dotted - ); + private void UpdateValueFromTextField (object? sender, FocusEventArgs e) + { + if (_tfHex == null) + { + return; } - foreach (KeyValuePair p in lc.GetMap ()) + if (Color.TryParse (_tfHex.Text, out Color? newColor)) { - AddRune (p.Key.X, p.Key.Y, p.Value); + SelectedColor = newColor.Value; + } + else + { + // value is invalid, revert the value in the text field back to current state + SyncSubViewValues (false); } } } diff --git a/Terminal.Gui/Views/ColorPicker16.cs b/Terminal.Gui/Views/ColorPicker16.cs new file mode 100644 index 000000000..a413c8633 --- /dev/null +++ b/Terminal.Gui/Views/ColorPicker16.cs @@ -0,0 +1,270 @@ +namespace Terminal.Gui; + +/// The Color picker. +public class ColorPicker16 : View +{ + /// Initializes a new instance of . + public ColorPicker16 () { SetInitialProperties (); } + + /// Columns of color boxes + private readonly int _cols = 8; + + /// Rows of color boxes + private readonly int _rows = 2; + + private int _boxHeight = 2; + private int _boxWidth = 4; + private int _selectColorIndex = (int)Color.Black; + + /// Height of a color box + public int BoxHeight + { + get => _boxHeight; + set + { + if (_boxHeight != value) + { + _boxHeight = value; + Width = Dim.Auto (minimumContentDim: _boxWidth * _cols); + Height = Dim.Auto (minimumContentDim: _boxHeight * _rows); + SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows)); + SetNeedsLayout (); + } + } + } + + /// Width of a color box + public int BoxWidth + { + get => _boxWidth; + set + { + if (_boxWidth != value) + { + _boxWidth = value; + Width = Dim.Auto (minimumContentDim: _boxWidth * _cols); + Height = Dim.Auto (minimumContentDim: _boxHeight * _rows); + SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows)); + SetNeedsLayout (); + } + } + } + + /// Fired when a color is picked. + public event EventHandler ColorChanged; + + /// Cursor for the selected color. + public Point Cursor + { + get => new (_selectColorIndex % _cols, _selectColorIndex / _cols); + set + { + int colorIndex = value.Y * _cols + value.X; + SelectedColor = (ColorName)colorIndex; + } + } + + /// Moves the selected item index to the next row. + /// + public virtual bool MoveDown () + { + if (Cursor.Y < _rows - 1) + { + SelectedColor += _cols; + } + + return true; + } + + /// Moves the selected item index to the previous column. + /// + public virtual bool MoveLeft () + { + if (Cursor.X > 0) + { + SelectedColor--; + } + + return true; + } + + /// Moves the selected item index to the next column. + /// + public virtual bool MoveRight () + { + if (Cursor.X < _cols - 1) + { + SelectedColor++; + } + + return true; + } + + /// Moves the selected item index to the previous row. + /// + public virtual bool MoveUp () + { + if (Cursor.Y > 0) + { + SelectedColor -= _cols; + } + + return true; + } + + /// + public override void OnDrawContent (Rectangle viewport) + { + base.OnDrawContent (viewport); + + Driver.SetAttribute (HasFocus ? ColorScheme.Focus : GetNormalColor ()); + var colorIndex = 0; + + for (var y = 0; y < Math.Max (2, viewport.Height / BoxHeight); y++) + { + for (var x = 0; x < Math.Max (8, viewport.Width / BoxWidth); x++) + { + int foregroundColorIndex = y == 0 ? colorIndex + _cols : colorIndex - _cols; + + if (foregroundColorIndex > 15 || colorIndex > 15) + { + continue; + } + + Driver.SetAttribute (new ((ColorName)foregroundColorIndex, (ColorName)colorIndex)); + bool selected = x == Cursor.X && y == Cursor.Y; + DrawColorBox (x, y, selected); + colorIndex++; + } + } + } + + /// Selected color. + public ColorName SelectedColor + { + get => (ColorName)_selectColorIndex; + set + { + if (value == (ColorName)_selectColorIndex) + { + return; + } + + _selectColorIndex = (int)value; + + ColorChanged?.Invoke ( + this, + new (value) + ); + SetNeedsDisplay (); + } + } + + /// Add the commands. + private void AddCommands () + { + AddCommand (Command.Left, () => MoveLeft ()); + AddCommand (Command.Right, () => MoveRight ()); + AddCommand (Command.LineUp, () => MoveUp ()); + AddCommand (Command.LineDown, () => MoveDown ()); + } + + /// Add the KeyBindinds. + private void AddKeyBindings () + { + KeyBindings.Add (Key.CursorLeft, Command.Left); + KeyBindings.Add (Key.CursorRight, Command.Right); + KeyBindings.Add (Key.CursorUp, Command.LineUp); + KeyBindings.Add (Key.CursorDown, Command.LineDown); + } + + // TODO: Decouple Cursor from SelectedColor so that mouse press-and-hold can show the color under the cursor. + + private void ColorPicker_MouseClick (object sender, MouseEventEventArgs me) + { + // if (CanFocus) + { + Cursor = new (me.MouseEvent.Position.X / _boxWidth, me.MouseEvent.Position.Y / _boxHeight); + SetFocus (); + me.Handled = true; + } + } + + /// Draw a box for one color. + /// X location. + /// Y location + /// + private void DrawColorBox (int x, int y, bool selected) + { + var index = 0; + + for (var zoomedY = 0; zoomedY < BoxHeight; zoomedY++) + { + for (var zoomedX = 0; zoomedX < BoxWidth; zoomedX++) + { + Move (x * BoxWidth + zoomedX, y * BoxHeight + zoomedY); + Driver.AddRune ((Rune)' '); + index++; + } + } + + if (selected) + { + DrawFocusRect (new (x * BoxWidth, y * BoxHeight, BoxWidth, BoxHeight)); + } + } + + private void DrawFocusRect (Rectangle rect) + { + var lc = new LineCanvas (); + + if (rect.Width == 1) + { + lc.AddLine (rect.Location, rect.Height, Orientation.Vertical, LineStyle.Dotted); + } + else if (rect.Height == 1) + { + lc.AddLine (rect.Location, rect.Width, Orientation.Horizontal, LineStyle.Dotted); + } + else + { + lc.AddLine (rect.Location, rect.Width, Orientation.Horizontal, LineStyle.Dotted); + + lc.AddLine ( + rect.Location with { Y = rect.Location.Y + rect.Height - 1 }, + rect.Width, + Orientation.Horizontal, + LineStyle.Dotted + ); + + lc.AddLine (rect.Location, rect.Height, Orientation.Vertical, LineStyle.Dotted); + + lc.AddLine ( + rect.Location with { X = rect.Location.X + rect.Width - 1 }, + rect.Height, + Orientation.Vertical, + LineStyle.Dotted + ); + } + + foreach (KeyValuePair p in lc.GetMap ()) + { + AddRune (p.Key.X, p.Key.Y, p.Value); + } + } + + private void SetInitialProperties () + { + HighlightStyle = HighlightStyle.PressedOutside | HighlightStyle.Pressed; + + CanFocus = true; + AddCommands (); + AddKeyBindings (); + + Width = Dim.Auto (minimumContentDim: _boxWidth * _cols); + Height = Dim.Auto (minimumContentDim: _boxHeight * _rows); + SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows)); + + MouseClick += ColorPicker_MouseClick; + } +} diff --git a/Terminal.Gui/Views/ColorPickerStyle.cs b/Terminal.Gui/Views/ColorPickerStyle.cs new file mode 100644 index 000000000..80e1b820a --- /dev/null +++ b/Terminal.Gui/Views/ColorPickerStyle.cs @@ -0,0 +1,25 @@ +#nullable enable + +namespace Terminal.Gui; + +/// +/// Contains style settings for e.g. which +/// to use. +/// +public class ColorPickerStyle +{ + /// + /// The color model for picking colors by RGB, HSV, etc. + /// + public ColorModel ColorModel { get; set; } = ColorModel.HSV; + + /// + /// True to put the numerical value of bars on the right of the color bar + /// + public bool ShowTextFields { get; set; } = true; + + /// + /// True to show an editable text field indicating the w3c/console color name of selected color. + /// + public bool ShowColorName { get; set; } = false; +} diff --git a/Terminal.Gui/Views/GBar.cs b/Terminal.Gui/Views/GBar.cs new file mode 100644 index 000000000..c3f1ec6e6 --- /dev/null +++ b/Terminal.Gui/Views/GBar.cs @@ -0,0 +1,27 @@ +#nullable enable + +using ColorHelper; + +namespace Terminal.Gui; + +internal class GBar : ColorBar +{ + public BBar? BBar { get; set; } + public RBar? RBar { get; set; } + + /// + protected override Color GetColor (double fraction) + { + if (RBar == null || BBar == null) + { + throw new ($"{nameof (GBar)} has not been set up correctly before drawing"); + } + + var rgb = new RGB ((byte)RBar.Value, (byte)(MaxValue * fraction), (byte)BBar.Value); + + return new (rgb.R, rgb.G, rgb.B); + } + + /// + protected override int MaxValue => 255; +} diff --git a/Terminal.Gui/Views/HueBar.cs b/Terminal.Gui/Views/HueBar.cs new file mode 100644 index 000000000..0968b51be --- /dev/null +++ b/Terminal.Gui/Views/HueBar.cs @@ -0,0 +1,21 @@ +#nullable enable + +using ColorHelper; +using ColorConverter = ColorHelper.ColorConverter; + +namespace Terminal.Gui; + +internal class HueBar : ColorBar +{ + /// + protected override Color GetColor (double fraction) + { + var hsl = new HSL ((int)(MaxValue * fraction), 100, 50); + RGB rgb = ColorConverter.HslToRgb (hsl); + + return new (rgb.R, rgb.G, rgb.B); + } + + /// + protected override int MaxValue => 360; +} diff --git a/Terminal.Gui/Views/IColorBar.cs b/Terminal.Gui/Views/IColorBar.cs new file mode 100644 index 000000000..f825e70ad --- /dev/null +++ b/Terminal.Gui/Views/IColorBar.cs @@ -0,0 +1,14 @@ +namespace Terminal.Gui; + +internal interface IColorBar +{ + int Value { get; set; } + + /// + /// Update the value of and reflect + /// changes in UI state but do not raise a value changed + /// event (to avoid circular events). + /// + /// + internal void SetValueWithoutRaisingEvent (int v); +} diff --git a/Terminal.Gui/Views/LightnessBar.cs b/Terminal.Gui/Views/LightnessBar.cs new file mode 100644 index 000000000..bc32e1549 --- /dev/null +++ b/Terminal.Gui/Views/LightnessBar.cs @@ -0,0 +1,29 @@ +#nullable enable + +using ColorHelper; +using ColorConverter = ColorHelper.ColorConverter; + +namespace Terminal.Gui; + +internal class LightnessBar : ColorBar +{ + public HueBar? HBar { get; set; } + public SaturationBar? SBar { get; set; } + + /// + protected override Color GetColor (double fraction) + { + if (HBar == null || SBar == null) + { + throw new ($"{nameof (LightnessBar)} has not been set up correctly before drawing"); + } + + var hsl = new HSL (HBar.Value, (byte)SBar.Value, (byte)(MaxValue * fraction)); + RGB rgb = ColorConverter.HslToRgb (hsl); + + return new (rgb.R, rgb.G, rgb.B); + } + + /// + protected override int MaxValue => 100; +} diff --git a/Terminal.Gui/Views/RBar.cs b/Terminal.Gui/Views/RBar.cs new file mode 100644 index 000000000..99651ec66 --- /dev/null +++ b/Terminal.Gui/Views/RBar.cs @@ -0,0 +1,27 @@ +#nullable enable + +using ColorHelper; + +namespace Terminal.Gui; + +internal class RBar : ColorBar +{ + public BBar? BBar { get; set; } + public GBar? GBar { get; set; } + + /// + protected override Color GetColor (double fraction) + { + if (GBar == null || BBar == null) + { + throw new ($"{nameof (RBar)} has not been set up correctly before drawing"); + } + + var rgb = new RGB ((byte)(MaxValue * fraction), (byte)GBar.Value, (byte)BBar.Value); + + return new (rgb.R, rgb.G, rgb.B); + } + + /// + protected override int MaxValue => 255; +} diff --git a/Terminal.Gui/Views/SaturationBar.cs b/Terminal.Gui/Views/SaturationBar.cs new file mode 100644 index 000000000..bd25fc82f --- /dev/null +++ b/Terminal.Gui/Views/SaturationBar.cs @@ -0,0 +1,40 @@ +#nullable enable + +using ColorHelper; +using ColorConverter = ColorHelper.ColorConverter; + +namespace Terminal.Gui; + +internal class SaturationBar : ColorBar +{ + public HueBar? HBar { get; set; } + + // Should only have either LBar or VBar not both + public LightnessBar? LBar { get; set; } + public ValueBar? VBar { get; set; } + + /// + protected override Color GetColor (double fraction) + { + if (LBar != null && HBar != null) + { + var hsl = new HSL (HBar.Value, (byte)(MaxValue * fraction), (byte)LBar.Value); + RGB rgb = ColorConverter.HslToRgb (hsl); + + return new (rgb.R, rgb.G, rgb.B); + } + + if (VBar != null && HBar != null) + { + var hsv = new HSV (HBar.Value, (byte)(MaxValue * fraction), (byte)VBar.Value); + RGB rgb = ColorConverter.HsvToRgb (hsv); + + return new (rgb.R, rgb.G, rgb.B); + } + + throw new ($"{nameof (SaturationBar)} requires either Lightness or Value to render"); + } + + /// + protected override int MaxValue => 100; +} diff --git a/Terminal.Gui/Views/ValueBar.cs b/Terminal.Gui/Views/ValueBar.cs new file mode 100644 index 000000000..0baced3d5 --- /dev/null +++ b/Terminal.Gui/Views/ValueBar.cs @@ -0,0 +1,29 @@ +#nullable enable + +using ColorHelper; +using ColorConverter = ColorHelper.ColorConverter; + +namespace Terminal.Gui; + +internal class ValueBar : ColorBar +{ + public HueBar? HBar { get; set; } + public SaturationBar? SBar { get; set; } + + /// + protected override Color GetColor (double fraction) + { + if (HBar == null || SBar == null) + { + throw new ($"{nameof (ValueBar)} has not been set up correctly before drawing"); + } + + var hsv = new HSV (HBar.Value, (byte)SBar.Value, (byte)(MaxValue * fraction)); + RGB rgb = ColorConverter.HsvToRgb (hsv); + + return new (rgb.R, rgb.G, rgb.B); + } + + /// + protected override int MaxValue => 100; +} diff --git a/UICatalog/Scenarios/AdornmentEditor.cs b/UICatalog/Scenarios/AdornmentEditor.cs index e941a8a84..8edd97f96 100644 --- a/UICatalog/Scenarios/AdornmentEditor.cs +++ b/UICatalog/Scenarios/AdornmentEditor.cs @@ -8,7 +8,7 @@ namespace UICatalog.Scenarios; /// public class AdornmentEditor : View { - private readonly ColorPicker _backgroundColorPicker = new () + private readonly ColorPicker16 _backgroundColorPicker = new () { Title = "_BG", BoxWidth = 1, @@ -18,7 +18,7 @@ public class AdornmentEditor : View Enabled = false }; - private readonly ColorPicker _foregroundColorPicker = new () + private readonly ColorPicker16 _foregroundColorPicker = new () { Title = "_FG", BoxWidth = 1, diff --git a/UICatalog/Scenarios/Adornments.cs b/UICatalog/Scenarios/Adornments.cs index 43d12a9a9..0f46fedd4 100644 --- a/UICatalog/Scenarios/Adornments.cs +++ b/UICatalog/Scenarios/Adornments.cs @@ -38,7 +38,7 @@ public class Adornments : Scenario app.Add (window); var tf1 = new TextField { Width = 10, Text = "TextField" }; - var color = new ColorPicker { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd () }; + var color = new ColorPicker16 { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd () }; color.BorderStyle = LineStyle.RoundedDotted; color.ColorChanged += (s, e) => @@ -47,7 +47,7 @@ public class Adornments : Scenario { Normal = new ( color.SuperView.ColorScheme.Normal.Foreground, - e.Color + e.CurrentValue ) }; }; diff --git a/UICatalog/Scenarios/ColorPicker.cs b/UICatalog/Scenarios/ColorPicker.cs index ae70a8a0d..04c58528d 100644 --- a/UICatalog/Scenarios/ColorPicker.cs +++ b/UICatalog/Scenarios/ColorPicker.cs @@ -23,6 +23,12 @@ public class ColorPickers : Scenario /// Foreground ColorPicker. private ColorPicker foregroundColorPicker; + /// Background ColorPicker. + private ColorPicker16 backgroundColorPicker16; + + /// Foreground ColorPicker. + private ColorPicker16 foregroundColorPicker16; + /// Setup the scenario. public override void Main () { @@ -33,8 +39,16 @@ public class ColorPickers : Scenario Title = GetQuitKeyAndName (), }; + /////////////////////////////////////// + // True Color Pickers + /////////////////////////////////////// + // Foreground ColorPicker. - foregroundColorPicker = new ColorPicker { Title = "Foreground Color", BorderStyle = LineStyle.Single }; + foregroundColorPicker = new ColorPicker { + Title = "Foreground Color", + BorderStyle = LineStyle.Single, + Width = Dim.Percent (50) + }; foregroundColorPicker.ColorChanged += ForegroundColor_ColorChanged; app.Add (foregroundColorPicker); @@ -49,9 +63,8 @@ public class ColorPickers : Scenario { Title = "Background Color", X = Pos.AnchorEnd (), - BoxHeight = 1, - BoxWidth = 4, - BorderStyle = LineStyle.Single, + Width = Dim.Percent (50), + BorderStyle = LineStyle.Single }; backgroundColorPicker.ColorChanged += BackgroundColor_ColorChanged; @@ -64,6 +77,37 @@ public class ColorPickers : Scenario app.Add (_backgroundColorLabel); + + /////////////////////////////////////// + // 16 Color Pickers + /////////////////////////////////////// + + + // Foreground ColorPicker 16. + foregroundColorPicker16 = new ColorPicker16 + { + Title = "Foreground Color", + BorderStyle = LineStyle.Single, + Width = Dim.Percent (50), + Visible = false // We default to HSV so hide old one + }; + foregroundColorPicker16.ColorChanged += ForegroundColor_ColorChanged; + app.Add (foregroundColorPicker16); + + // Background ColorPicker 16. + backgroundColorPicker16 = new ColorPicker16 + { + Title = "Background Color", + X = Pos.AnchorEnd (), + Width = Dim.Percent (50), + BorderStyle = LineStyle.Single, + Visible = false // We default to HSV so hide old one + }; + + backgroundColorPicker16.ColorChanged += BackgroundColor_ColorChanged; + app.Add (backgroundColorPicker16); + + // Demo Label. _demoView = new View { @@ -79,6 +123,98 @@ public class ColorPickers : Scenario }; app.Add (_demoView); + + // Radio for switching color models + var rgColorModel = new RadioGroup () + { + Y = Pos.Bottom (_demoView), + Width = Dim.Auto (), + Height = Dim.Auto (), + RadioLabels = new [] + { + "RGB", + "HSV", + "HSL", + "16 Colors" + }, + SelectedItem = (int)foregroundColorPicker.Style.ColorModel, + }; + + rgColorModel.SelectedItemChanged += (_, e) => + { + // 16 colors + if (e.SelectedItem == 3) + { + + foregroundColorPicker16.Visible = true; + foregroundColorPicker.Visible = false; + + backgroundColorPicker16.Visible = true; + backgroundColorPicker.Visible = false; + + // Switching to 16 colors + ForegroundColor_ColorChanged (null,null); + BackgroundColor_ColorChanged (null, null); + } + else + { + foregroundColorPicker16.Visible = false; + foregroundColorPicker.Visible = true; + foregroundColorPicker.Style.ColorModel = (ColorModel)e.SelectedItem; + foregroundColorPicker.ApplyStyleChanges (); + + backgroundColorPicker16.Visible = false; + backgroundColorPicker.Visible = true; + backgroundColorPicker.Style.ColorModel = (ColorModel)e.SelectedItem; + backgroundColorPicker.ApplyStyleChanges (); + + + // Switching to true colors + foregroundColorPicker.SelectedColor = foregroundColorPicker16.SelectedColor; + backgroundColorPicker.SelectedColor = backgroundColorPicker16.SelectedColor; + } + }; + + app.Add (rgColorModel); + + // Checkbox for switching show text fields on and off + var cbShowTextFields = new CheckBox () + { + Text = "Show Text Fields", + Y = Pos.Bottom (rgColorModel)+1, + Width = Dim.Auto (), + Height = Dim.Auto (), + CheckedState = foregroundColorPicker.Style.ShowTextFields ? CheckState.Checked: CheckState.UnChecked, + }; + + cbShowTextFields.CheckedStateChanging += (_, e) => + { + foregroundColorPicker.Style.ShowTextFields = e.NewValue == CheckState.Checked; + foregroundColorPicker.ApplyStyleChanges (); + backgroundColorPicker.Style.ShowTextFields = e.NewValue == CheckState.Checked; + backgroundColorPicker.ApplyStyleChanges (); + }; + app.Add (cbShowTextFields); + + // Checkbox for switching show text fields on and off + var cbShowName = new CheckBox () + { + Text = "Show Color Name", + Y = Pos.Bottom (cbShowTextFields) + 1, + Width = Dim.Auto (), + Height = Dim.Auto (), + CheckedState = foregroundColorPicker.Style.ShowColorName ? CheckState.Checked : CheckState.UnChecked, + }; + + cbShowName.CheckedStateChanging += (_, e) => + { + foregroundColorPicker.Style.ShowColorName = e.NewValue == CheckState.Checked; + foregroundColorPicker.ApplyStyleChanges (); + backgroundColorPicker.Style.ShowColorName = e.NewValue == CheckState.Checked; + backgroundColorPicker.ApplyStyleChanges (); + }; + app.Add (cbShowName); + // Set default colors. foregroundColorPicker.SelectedColor = _demoView.SuperView.ColorScheme.Normal.Foreground.GetClosestNamedColor (); backgroundColorPicker.SelectedColor = _demoView.SuperView.ColorScheme.Normal.Background.GetClosestNamedColor (); @@ -92,25 +228,32 @@ public class ColorPickers : Scenario /// Fired when background color is changed. private void BackgroundColor_ColorChanged (object sender, EventArgs e) { - UpdateColorLabel (_backgroundColorLabel, backgroundColorPicker); + UpdateColorLabel (_backgroundColorLabel, + backgroundColorPicker.Visible ? + backgroundColorPicker.SelectedColor : + backgroundColorPicker16.SelectedColor + ); UpdateDemoLabel (); } /// Fired when foreground color is changed. private void ForegroundColor_ColorChanged (object sender, EventArgs e) { - UpdateColorLabel (_foregroundColorLabel, foregroundColorPicker); + UpdateColorLabel (_foregroundColorLabel, + foregroundColorPicker.Visible ? + foregroundColorPicker.SelectedColor : + foregroundColorPicker16.SelectedColor + ); UpdateDemoLabel (); } /// Update a color label from his ColorPicker. - private void UpdateColorLabel (Label label, ColorPicker colorPicker) + private void UpdateColorLabel (Label label, Color color) { label.Clear (); - var color = new Color (colorPicker.SelectedColor); label.Text = - $"{colorPicker.SelectedColor} ({(int)colorPicker.SelectedColor}) #{color.R:X2}{color.G:X2}{color.B:X2}"; + $"{color} ({(int)color}) #{color.R:X2}{color.G:X2}{color.B:X2}"; } /// Update Demo Label. @@ -119,8 +262,12 @@ public class ColorPickers : Scenario _demoView.ColorScheme = new ColorScheme { Normal = new Attribute ( - foregroundColorPicker.SelectedColor, - backgroundColorPicker.SelectedColor + foregroundColorPicker.Visible ? + foregroundColorPicker.SelectedColor : + foregroundColorPicker16.SelectedColor, + backgroundColorPicker.Visible ? + backgroundColorPicker.SelectedColor : + backgroundColorPicker16.SelectedColor ) }; } diff --git a/UICatalog/Scenarios/ContentScrolling.cs b/UICatalog/Scenarios/ContentScrolling.cs index 57589459a..d94e0a7e4 100644 --- a/UICatalog/Scenarios/ContentScrolling.cs +++ b/UICatalog/Scenarios/ContentScrolling.cs @@ -326,7 +326,7 @@ public class ContentScrolling : Scenario // Add demo views to show that things work correctly var textField = new TextField { X = 20, Y = 7, Width = 15, Text = "Test TextField" }; - var colorPicker = new ColorPicker { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd (), Y = 10 }; + var colorPicker = new ColorPicker16 { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd (), Y = 10 }; colorPicker.BorderStyle = LineStyle.RoundedDotted; colorPicker.ColorChanged += (s, e) => @@ -335,7 +335,7 @@ public class ContentScrolling : Scenario { Normal = new ( colorPicker.SuperView.ColorScheme.Normal.Foreground, - e.Color + e.CurrentValue ) }; }; diff --git a/UICatalog/Scenarios/LineDrawing.cs b/UICatalog/Scenarios/LineDrawing.cs index 546487a72..6302f386d 100644 --- a/UICatalog/Scenarios/LineDrawing.cs +++ b/UICatalog/Scenarios/LineDrawing.cs @@ -1,10 +1,104 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using Terminal.Gui; namespace UICatalog.Scenarios; +public interface ITool +{ + void OnMouseEvent (DrawingArea area, MouseEvent mouseEvent); +} + +internal class DrawLineTool : ITool +{ + private StraightLine _currentLine; + public LineStyle LineStyle { get; set; } = LineStyle.Single; + + /// + public void OnMouseEvent (DrawingArea area, MouseEvent mouseEvent) + { + if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) + { + if (_currentLine == null) + { + // Mouse pressed down + _currentLine = new ( + mouseEvent.Position, + 0, + Orientation.Vertical, + LineStyle, + area.CurrentAttribute + ); + + area.CurrentLayer.AddLine (_currentLine); + } + else + { + // Mouse dragged + Point start = _currentLine.Start; + Point end = mouseEvent.Position; + var orientation = Orientation.Vertical; + int length = end.Y - start.Y; + + // if line is wider than it is tall switch to horizontal + if (Math.Abs (start.X - end.X) > Math.Abs (start.Y - end.Y)) + { + orientation = Orientation.Horizontal; + length = end.X - start.X; + } + + if (length > 0) + { + length++; + } + else + { + length--; + } + + _currentLine.Length = length; + _currentLine.Orientation = orientation; + area.CurrentLayer.ClearCache (); + area.SetNeedsDisplay (); + } + } + else + { + // Mouse released + if (_currentLine != null) + { + if (_currentLine.Length == 0) + { + _currentLine.Length = 1; + } + + if (_currentLine.Style == LineStyle.None) + { + // Treat none as eraser + int idx = area.Layers.IndexOf (area.CurrentLayer); + area.Layers.Remove (area.CurrentLayer); + + area.CurrentLayer = new ( + area.CurrentLayer.Lines.Exclude ( + _currentLine.Start, + _currentLine.Length, + _currentLine.Orientation + ) + ); + + area.Layers.Insert (idx, area.CurrentLayer); + } + + _currentLine = null; + area.ClearUndo (); + area.SetNeedsDisplay (); + } + } + } +} + [ScenarioMetadata ("Line Drawing", "Demonstrates LineCanvas.")] [ScenarioCategory ("Controls")] [ScenarioCategory ("Drawing")] @@ -18,12 +112,14 @@ public class LineDrawing : Scenario var tools = new ToolsView { Title = "Tools", X = Pos.Right (canvas) - 20, Y = 2 }; - tools.ColorChanged += c => canvas.SetColor (c); - tools.SetStyle += b => canvas.LineStyle = b; + tools.ColorChanged += (s, e) => canvas.SetAttribute (e); + tools.SetStyle += b => canvas.CurrentTool = new DrawLineTool { LineStyle = b }; tools.AddLayer += () => canvas.AddLayer (); win.Add (canvas); win.Add (tools); + tools.CurrentColor = canvas.GetNormalColor (); + canvas.CurrentAttribute = tools.CurrentColor; win.KeyDown += (s, e) => { e.Handled = canvas.OnKeyDown (e); }; @@ -32,206 +128,329 @@ public class LineDrawing : Scenario Application.Shutdown (); } - private class DrawingArea : View + public static bool PromptForColor (string title, Color current, out Color newColor) { - private readonly List _layers = new (); - private readonly Stack _undoHistory = new (); - private Color _currentColor = new (Color.White); - private LineCanvas _currentLayer; - private StraightLine _currentLine; - public DrawingArea () { AddLayer (); } - public LineStyle LineStyle { get; set; } + var accept = false; - public override void OnDrawContentComplete (Rectangle viewport) + var d = new Dialog { - base.OnDrawContentComplete (viewport); + Title = title, + Height = 7 + }; - foreach (LineCanvas canvas in _layers) + var btnOk = new Button + { + X = Pos.Center () - 5, + Y = 4, + Text = "Ok", + Width = Dim.Auto (), + IsDefault = true + }; + + btnOk.Accept += (s, e) => + { + accept = true; + e.Handled = true; + Application.RequestStop (); + }; + + var btnCancel = new Button + { + X = Pos.Center () + 5, + Y = 4, + Text = "Cancel", + Width = Dim.Auto () + }; + + btnCancel.Accept += (s, e) => + { + e.Handled = true; + Application.RequestStop (); + }; + + d.Add (btnOk); + d.Add (btnCancel); + + /* Does not work + d.AddButton (btnOk); + d.AddButton (btnCancel); + */ + var cp = new ColorPicker + { + SelectedColor = current, + Width = Dim.Fill () + }; + + d.Add (cp); + + Application.Run (d); + d.Dispose (); + newColor = cp.SelectedColor; + + return accept; + } +} + +public class ToolsView : Window +{ + private Button _addLayerBtn; + private readonly AttributeView _colors; + private RadioGroup _stylePicker; + + public Attribute CurrentColor + { + get => _colors.Value; + set => _colors.Value = value; + } + + public ToolsView () + { + BorderStyle = LineStyle.Dotted; + Border.Thickness = new (1, 2, 1, 1); + Initialized += ToolsView_Initialized; + _colors = new (); + } + + public event Action AddLayer; + + public override void BeginInit () + { + base.BeginInit (); + + _colors.ValueChanged += (s, e) => ColorChanged?.Invoke (this, e); + + _stylePicker = new() + { + X = 0, Y = Pos.Bottom (_colors), RadioLabels = Enum.GetNames (typeof (LineStyle)).ToArray () + }; + _stylePicker.SelectedItemChanged += (s, a) => { SetStyle?.Invoke ((LineStyle)a.SelectedItem); }; + _stylePicker.SelectedItem = 1; + + _addLayerBtn = new() { Text = "New Layer", X = Pos.Center (), Y = Pos.Bottom (_stylePicker) }; + + _addLayerBtn.Accept += (s, a) => AddLayer?.Invoke (); + Add (_colors, _stylePicker, _addLayerBtn); + } + + public event EventHandler ColorChanged; + public event Action SetStyle; + + private void ToolsView_Initialized (object sender, EventArgs e) + { + LayoutSubviews (); + + Width = Math.Max (_colors.Frame.Width, _stylePicker.Frame.Width) + GetAdornmentsThickness ().Horizontal; + + Height = _colors.Frame.Height + _stylePicker.Frame.Height + _addLayerBtn.Frame.Height + GetAdornmentsThickness ().Vertical; + SuperView.LayoutSubviews (); + } +} + +public class DrawingArea : View +{ + public readonly List Layers = new (); + private readonly Stack _undoHistory = new (); + public Attribute CurrentAttribute { get; set; } + public LineCanvas CurrentLayer { get; set; } + + public ITool CurrentTool { get; set; } = new DrawLineTool (); + public DrawingArea () { AddLayer (); } + + public override void OnDrawContentComplete (Rectangle viewport) + { + base.OnDrawContentComplete (viewport); + + foreach (LineCanvas canvas in Layers) + { + foreach (KeyValuePair c in canvas.GetCellMap ()) { - foreach (KeyValuePair c in canvas.GetCellMap ()) + if (c.Value is { }) { - if (c.Value is { }) - { - Driver.SetAttribute (c.Value.Value.Attribute ?? ColorScheme.Normal); + Driver.SetAttribute (c.Value.Value.Attribute ?? ColorScheme.Normal); - // TODO: #2616 - Support combining sequences that don't normalize - AddRune (c.Key.X, c.Key.Y, c.Value.Value.Rune); - } + // TODO: #2616 - Support combining sequences that don't normalize + AddRune (c.Key.X, c.Key.Y, c.Value.Value.Rune); } } } + } - //// BUGBUG: Why is this not handled by a key binding??? - public override bool OnKeyDown (Key e) + //// BUGBUG: Why is this not handled by a key binding??? + public override bool OnKeyDown (Key e) + { + // BUGBUG: These should be implemented with key bindings + if (e.KeyCode == (KeyCode.Z | KeyCode.CtrlMask)) { - // BUGBUG: These should be implemented with key bindings - if (e.KeyCode == (KeyCode.Z | KeyCode.CtrlMask)) + StraightLine pop = CurrentLayer.RemoveLastLine (); + + if (pop != null) { - StraightLine pop = _currentLayer.RemoveLastLine (); + _undoHistory.Push (pop); + SetNeedsDisplay (); - if (pop != null) - { - _undoHistory.Push (pop); - SetNeedsDisplay (); - - return true; - } + return true; } - - if (e.KeyCode == (KeyCode.Y | KeyCode.CtrlMask)) - { - if (_undoHistory.Any ()) - { - StraightLine pop = _undoHistory.Pop (); - _currentLayer.AddLine (pop); - SetNeedsDisplay (); - - return true; - } - } - - return false; } - protected override bool OnMouseEvent (MouseEvent mouseEvent) + if (e.KeyCode == (KeyCode.Y | KeyCode.CtrlMask)) { - if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) + if (_undoHistory.Any ()) { - if (_currentLine == null) - { - // Mouse pressed down - _currentLine = new StraightLine ( - mouseEvent.Position, - 0, - Orientation.Vertical, - LineStyle, - new Attribute (_currentColor, GetNormalColor ().Background) - ); + StraightLine pop = _undoHistory.Pop (); + CurrentLayer.AddLine (pop); + SetNeedsDisplay (); - _currentLayer.AddLine (_currentLine); - } - else - { - // Mouse dragged - Point start = _currentLine.Start; - var end = mouseEvent.Position; - var orientation = Orientation.Vertical; - int length = end.Y - start.Y; + return true; + } + } - // if line is wider than it is tall switch to horizontal - if (Math.Abs (start.X - end.X) > Math.Abs (start.Y - end.Y)) - { - orientation = Orientation.Horizontal; - length = end.X - start.X; - } + return false; + } - if (length > 0) - { - length++; - } - else - { - length--; - } + protected override bool OnMouseEvent (MouseEvent mouseEvent) + { + CurrentTool.OnMouseEvent (this, mouseEvent); - _currentLine.Length = length; - _currentLine.Orientation = orientation; - _currentLayer.ClearCache (); - SetNeedsDisplay (); - } + return base.OnMouseEvent (mouseEvent); + } + + internal void AddLayer () + { + CurrentLayer = new (); + Layers.Add (CurrentLayer); + } + + internal void SetAttribute (Attribute a) { CurrentAttribute = a; } + + public void ClearUndo () { _undoHistory.Clear (); } +} + +public class AttributeView : View +{ + public event EventHandler ValueChanged; + private Attribute _value; + + public Attribute Value + { + get => _value; + set + { + _value = value; + ValueChanged?.Invoke (this, value); + } + } + + private static readonly HashSet<(int, int)> ForegroundPoints = new() + { + (0, 0), (1, 0), (2, 0), + (0, 1), (1, 1), (2, 1) + }; + + private static readonly HashSet<(int, int)> BackgroundPoints = new() + { + (3, 1), + (1, 2), (2, 2), (3, 2) + }; + + public AttributeView () + { + Width = 4; + Height = 3; + } + + /// + public override void OnDrawContent (Rectangle viewport) + { + base.OnDrawContent (viewport); + + Color fg = Value.Foreground; + Color bg = Value.Background; + + bool isTransparentFg = fg == GetNormalColor ().Background; + bool isTransparentBg = bg == GetNormalColor ().Background; + + Driver.SetAttribute (new (fg, isTransparentFg ? Color.Gray : fg)); + + // Square of foreground color + foreach ((int, int) point in ForegroundPoints) + { + // Make pattern like this when it is same color as background of control + /*▓▒ + ▒▓*/ + Rune rune; + + if (isTransparentFg) + { + rune = (Rune)(point.Item1 % 2 == point.Item2 % 2 ? '▓' : '▒'); } else { - // Mouse released - if (_currentLine != null) - { - if (_currentLine.Length == 0) - { - _currentLine.Length = 1; - } - - if (_currentLine.Style == LineStyle.None) - { - // Treat none as eraser - int idx = _layers.IndexOf (_currentLayer); - _layers.Remove (_currentLayer); - - _currentLayer = new LineCanvas ( - _currentLayer.Lines.Exclude ( - _currentLine.Start, - _currentLine.Length, - _currentLine.Orientation - ) - ); - - _layers.Insert (idx, _currentLayer); - } - - _currentLine = null; - _undoHistory.Clear (); - SetNeedsDisplay (); - } + rune = (Rune)'█'; } - return base.OnMouseEvent (mouseEvent); + AddRune (point.Item1, point.Item2, rune); } - internal void AddLayer () + Driver.SetAttribute (new (bg, isTransparentBg ? Color.Gray : bg)); + + // Square of background color + foreach ((int, int) point in BackgroundPoints) { - _currentLayer = new LineCanvas (); - _layers.Add (_currentLayer); - } + // Make pattern like this when it is same color as background of control + /*▓▒ + ▒▓*/ + Rune rune; - internal void SetColor (Color c) { _currentColor = c; } + if (isTransparentBg) + { + rune = (Rune)(point.Item1 % 2 == point.Item2 % 2 ? '▓' : '▒'); + } + else + { + rune = (Rune)'█'; + } + + AddRune (point.Item1, point.Item2, rune); + } } - private class ToolsView : Window + /// + protected override bool OnMouseEvent (MouseEvent mouseEvent) { - private Button _addLayerBtn; - private ColorPicker _colorPicker; - private RadioGroup _stylePicker; - - public ToolsView () + if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) { - BorderStyle = LineStyle.Dotted; - Border.Thickness = new Thickness (1, 2, 1, 1); - Initialized += ToolsView_Initialized; - } - - public event Action AddLayer; - - public override void BeginInit () - { - base.BeginInit (); - - _colorPicker = new ColorPicker { X = 0, Y = 0, BoxHeight = 1, BoxWidth = 2 }; - - _colorPicker.ColorChanged += (s, a) => ColorChanged?.Invoke (a.Color); - - _stylePicker = new RadioGroup + if (IsForegroundPoint (mouseEvent.Position.X, mouseEvent.Position.Y)) { - X = 0, Y = Pos.Bottom (_colorPicker), RadioLabels = Enum.GetNames (typeof (LineStyle)).ToArray () - }; - _stylePicker.SelectedItemChanged += (s, a) => { SetStyle?.Invoke ((LineStyle)a.SelectedItem); }; - _stylePicker.SelectedItem = 1; - - _addLayerBtn = new Button { Text = "New Layer", X = Pos.Center (), Y = Pos.Bottom (_stylePicker) }; - - _addLayerBtn.Accept += (s, a) => AddLayer?.Invoke (); - Add (_colorPicker, _stylePicker, _addLayerBtn); + ClickedInForeground (); + } + else if (IsBackgroundPoint (mouseEvent.Position.X, mouseEvent.Position.Y)) + { + ClickedInBackground (); + } } - public event Action ColorChanged; - public event Action SetStyle; + return base.OnMouseEvent (mouseEvent); + } - private void ToolsView_Initialized (object sender, EventArgs e) + private bool IsForegroundPoint (int x, int y) { return ForegroundPoints.Contains ((x, y)); } + + private bool IsBackgroundPoint (int x, int y) { return BackgroundPoints.Contains ((x, y)); } + + private void ClickedInBackground () + { + if (LineDrawing.PromptForColor ("Background", Value.Background, out Color newColor)) { - LayoutSubviews (); + Value = new (Value.Foreground, newColor); + SetNeedsDisplay (); + } + } - Width = Math.Max (_colorPicker.Frame.Width, _stylePicker.Frame.Width) + GetAdornmentsThickness ().Horizontal; - - Height = _colorPicker.Frame.Height + _stylePicker.Frame.Height + _addLayerBtn.Frame.Height + GetAdornmentsThickness ().Vertical; - SuperView.LayoutSubviews (); + private void ClickedInForeground () + { + if (LineDrawing.PromptForColor ("Foreground", Value.Foreground, out Color newColor)) + { + Value = new (newColor, Value.Background); + SetNeedsDisplay (); } } } diff --git a/UICatalog/Scenarios/ProgressBarStyles.cs b/UICatalog/Scenarios/ProgressBarStyles.cs index 705b68d68..d863c33c3 100644 --- a/UICatalog/Scenarios/ProgressBarStyles.cs +++ b/UICatalog/Scenarios/ProgressBarStyles.cs @@ -63,55 +63,25 @@ public class ProgressBarStyles : Scenario #region ColorPicker - ColorName ChooseColor (string text, ColorName colorName) - { - var colorPicker = new ColorPicker { Title = text, SelectedColor = colorName }; - - var dialog = new Dialog { Title = text }; - - dialog.Initialized += (sender, args) => - { - // TODO: Replace with Dim.Auto - dialog.X = _pbList.Frame.X; - dialog.Y = _pbList.Frame.Height; - }; - - dialog.LayoutComplete += (sender, args) => - { - dialog.Viewport = Rectangle.Empty with - { - Width = colorPicker.Frame.Width, - Height = colorPicker.Frame.Height - }; - Application.Top.LayoutSubviews (); - }; - - dialog.Add (colorPicker); - colorPicker.ColorChanged += (s, e) => { dialog.RequestStop (); }; - Application.Run (dialog); - dialog.Dispose (); - - ColorName retColor = colorPicker.SelectedColor; - colorPicker.Dispose (); - - return retColor; - } var fgColorPickerBtn = new Button { Text = "Foreground HotNormal Color", X = Pos.Center (), - Y = Pos.Align (Alignment.Start), + Y = Pos.Align (Alignment.Start) }; container.Add (fgColorPickerBtn); fgColorPickerBtn.Accept += (s, e) => { - ColorName newColor = ChooseColor ( - fgColorPickerBtn.Text, - editor.ViewToEdit.ColorScheme.HotNormal.Foreground - .GetClosestNamedColor () - ); + if (!LineDrawing.PromptForColor ( + fgColorPickerBtn.Text, + editor.ViewToEdit.ColorScheme.HotNormal.Foreground, + out var newColor + )) + { + return; + } var cs = new ColorScheme (editor.ViewToEdit.ColorScheme) { @@ -134,11 +104,14 @@ public class ProgressBarStyles : Scenario bgColorPickerBtn.Accept += (s, e) => { - ColorName newColor = ChooseColor ( - fgColorPickerBtn.Text, - editor.ViewToEdit.ColorScheme.HotNormal.Background - .GetClosestNamedColor () - ); + if (!LineDrawing.PromptForColor ( + fgColorPickerBtn.Text, + editor.ViewToEdit.ColorScheme.HotNormal.Background + , out var newColor)) + + { + return; + } var cs = new ColorScheme (editor.ViewToEdit.ColorScheme) { diff --git a/UICatalog/Scenarios/Shortcuts.cs b/UICatalog/Scenarios/Shortcuts.cs index 11313f5c4..15cbe7864 100644 --- a/UICatalog/Scenarios/Shortcuts.cs +++ b/UICatalog/Scenarios/Shortcuts.cs @@ -318,17 +318,17 @@ public class Shortcuts : Scenario CanFocus = false }; - var bgColor = new ColorPicker () + var bgColor = new ColorPicker16 () { - CanFocus = false, BoxHeight = 1, BoxWidth = 1, + CanFocus = false }; bgColor.ColorChanged += (o, args) => { Application.Top.ColorScheme = new ColorScheme (Application.Top.ColorScheme) { - Normal = new Attribute (Application.Top.ColorScheme.Normal.Foreground, args.Color), + Normal = new Attribute (Application.Top.ColorScheme.Normal.Foreground, args.CurrentValue), }; }; hShortcutBG.CommandView = bgColor; diff --git a/UnitTests/Views/ColorPicker16Tests.cs b/UnitTests/Views/ColorPicker16Tests.cs new file mode 100644 index 000000000..a25ccbb1a --- /dev/null +++ b/UnitTests/Views/ColorPicker16Tests.cs @@ -0,0 +1,79 @@ +namespace Terminal.Gui.ViewsTests; + +public class ColorPicker16Tests +{ + [Fact] + public void Constructors () + { + var colorPicker = new ColorPicker16 (); + Assert.Equal (ColorName.Black, colorPicker.SelectedColor); + Assert.Equal (Point.Empty, colorPicker.Cursor); + Assert.True (colorPicker.CanFocus); + + colorPicker.BeginInit (); + colorPicker.EndInit (); + colorPicker.LayoutSubviews (); + Assert.Equal (new (0, 0, 32, 4), colorPicker.Frame); + } + + [Fact] + public void KeyBindings_Command () + { + var colorPicker = new ColorPicker16 (); + Assert.Equal (ColorName.Black, colorPicker.SelectedColor); + + Assert.True (colorPicker.NewKeyDownEvent (Key.CursorRight)); + Assert.Equal (ColorName.Blue, colorPicker.SelectedColor); + + Assert.True (colorPicker.NewKeyDownEvent (Key.CursorDown)); + Assert.Equal (ColorName.BrightBlue, colorPicker.SelectedColor); + + Assert.True (colorPicker.NewKeyDownEvent (Key.CursorLeft)); + Assert.Equal (ColorName.DarkGray, colorPicker.SelectedColor); + + Assert.True (colorPicker.NewKeyDownEvent (Key.CursorUp)); + Assert.Equal (ColorName.Black, colorPicker.SelectedColor); + + Assert.True (colorPicker.NewKeyDownEvent (Key.CursorLeft)); + Assert.Equal (ColorName.Black, colorPicker.SelectedColor); + + Assert.True (colorPicker.NewKeyDownEvent (Key.CursorUp)); + Assert.Equal (ColorName.Black, colorPicker.SelectedColor); + } + + [Fact] + [AutoInitShutdown] + public void MouseEvents () + { + var colorPicker = new ColorPicker16 { X = 0, Y = 0, Height = 4, Width = 32 }; + Assert.Equal (ColorName.Black, colorPicker.SelectedColor); + var top = new Toplevel (); + top.Add (colorPicker); + Application.Begin (top); + + Assert.False (colorPicker.NewMouseEvent (new ())); + + Assert.True (colorPicker.NewMouseEvent (new () { Position = new (4, 1), Flags = MouseFlags.Button1Clicked })); + Assert.Equal (ColorName.Blue, colorPicker.SelectedColor); + top.Dispose (); + } + + [Fact] + public void SelectedColorAndCursor () + { + var colorPicker = new ColorPicker16 (); + colorPicker.SelectedColor = ColorName.White; + Assert.Equal (7, colorPicker.Cursor.X); + Assert.Equal (1, colorPicker.Cursor.Y); + + colorPicker.SelectedColor = Color.Black; + Assert.Equal (0, colorPicker.Cursor.X); + Assert.Equal (0, colorPicker.Cursor.Y); + + colorPicker.Cursor = new (7, 1); + Assert.Equal (ColorName.White, colorPicker.SelectedColor); + + colorPicker.Cursor = Point.Empty; + Assert.Equal (ColorName.Black, colorPicker.SelectedColor); + } +} \ No newline at end of file diff --git a/UnitTests/Views/ColorPickerTests.cs b/UnitTests/Views/ColorPickerTests.cs index bc9410a62..af0d2099a 100644 --- a/UnitTests/Views/ColorPickerTests.cs +++ b/UnitTests/Views/ColorPickerTests.cs @@ -1,79 +1,766 @@ -namespace Terminal.Gui.ViewsTests; +using Xunit.Abstractions; +using Color = Terminal.Gui.Color; + +namespace Terminal.Gui.ViewsTests; public class ColorPickerTests { [Fact] - public void Constructors () + [SetupFakeDriver] + public void ColorPicker_Construct_DefaultValue () { - var colorPicker = new ColorPicker (); - Assert.Equal (ColorName.Black, colorPicker.SelectedColor); - Assert.Equal (Point.Empty, colorPicker.Cursor); - Assert.True (colorPicker.CanFocus); + var cp = GetColorPicker (ColorModel.HSV, false); - colorPicker.BeginInit (); - colorPicker.EndInit (); - colorPicker.LayoutSubviews (); - Assert.Equal (new (0, 0, 32, 4), colorPicker.Frame); + // Should be only a single text field (Hex) because ShowTextFields is false + Assert.Single (cp.Subviews.OfType ()); + + cp.Draw (); + + // All bars should be at 0 with the triangle at 0 (+2 because of "H:", "S:" etc) + var h = GetColorBar (cp, ColorPickerPart.Bar1); + Assert.Equal ("H:", h.Text); + Assert.Equal (2, h.TrianglePosition); + Assert.IsType (h); + + var s = GetColorBar (cp, ColorPickerPart.Bar2); + Assert.Equal ("S:", s.Text); + Assert.Equal (2, s.TrianglePosition); + Assert.IsType (s); + + var v = GetColorBar (cp, ColorPickerPart.Bar3); + Assert.Equal ("V:", v.Text); + Assert.Equal (2, v.TrianglePosition); + Assert.IsType (v); + + var hex = GetTextField (cp, ColorPickerPart.Hex); + Assert.Equal ("#000000", hex.Text); } [Fact] - public void KeyBindings_Command () + [SetupFakeDriver] + public void ColorPicker_RGB_KeyboardNavigation () { - var colorPicker = new ColorPicker (); - Assert.Equal (ColorName.Black, colorPicker.SelectedColor); + var cp = GetColorPicker (ColorModel.RGB, false); + cp.Draw (); - Assert.True (colorPicker.NewKeyDownEvent (Key.CursorRight)); - Assert.Equal (ColorName.Blue, colorPicker.SelectedColor); + var r = GetColorBar (cp, ColorPickerPart.Bar1); + var g = GetColorBar (cp, ColorPickerPart.Bar2); + var b = GetColorBar (cp, ColorPickerPart.Bar3); + var hex = GetTextField (cp, ColorPickerPart.Hex); - Assert.True (colorPicker.NewKeyDownEvent (Key.CursorDown)); - Assert.Equal (ColorName.BrightBlue, colorPicker.SelectedColor); + Assert.Equal ("R:", r.Text); + Assert.Equal (2, r.TrianglePosition); + Assert.IsType (r); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.IsType (g); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.IsType (b); + Assert.Equal ("#000000", hex.Text); - Assert.True (colorPicker.NewKeyDownEvent (Key.CursorLeft)); - Assert.Equal (ColorName.DarkGray, colorPicker.SelectedColor); + Assert.IsAssignableFrom (cp.Focused); - Assert.True (colorPicker.NewKeyDownEvent (Key.CursorUp)); - Assert.Equal (ColorName.Black, colorPicker.SelectedColor); + cp.Draw (); - Assert.True (colorPicker.NewKeyDownEvent (Key.CursorLeft)); - Assert.Equal (ColorName.Black, colorPicker.SelectedColor); + Application.OnKeyDown (Key.CursorRight); - Assert.True (colorPicker.NewKeyDownEvent (Key.CursorUp)); - Assert.Equal (ColorName.Black, colorPicker.SelectedColor); + cp.Draw (); + + Assert.Equal (3, r.TrianglePosition); + Assert.Equal ("#0F0000", hex.Text); + + Application.OnKeyDown (Key.CursorRight); + + cp.Draw (); + + Assert.Equal (4, r.TrianglePosition); + Assert.Equal ("#1E0000", hex.Text); + + // Use cursor to move the triangle all the way to the right + for (int i = 0; i < 1000; i++) + { + Application.OnKeyDown (Key.CursorRight); + } + + cp.Draw (); + + // 20 width and TrianglePosition is 0 indexed + // Meaning we are asserting that triangle is at end + Assert.Equal (19, r.TrianglePosition); + Assert.Equal ("#FF0000", hex.Text); + + Application.Current.Dispose (); } [Fact] - [AutoInitShutdown] - public void MouseEvents () + [SetupFakeDriver] + public void ColorPicker_RGB_MouseNavigation () { - var colorPicker = new ColorPicker { X = 0, Y = 0, Height = 4, Width = 32 }; - Assert.Equal (ColorName.Black, colorPicker.SelectedColor); - var top = new Toplevel (); - top.Add (colorPicker); - Application.Begin (top); + var cp = GetColorPicker (ColorModel.RGB,false); - Assert.False (colorPicker.NewMouseEvent (new ())); + cp.Draw (); - Assert.True (colorPicker.NewMouseEvent (new() { Position = new (4, 1), Flags = MouseFlags.Button1Clicked })); - Assert.Equal (ColorName.Blue, colorPicker.SelectedColor); - top.Dispose (); + var r = GetColorBar (cp, ColorPickerPart.Bar1); + var g = GetColorBar (cp, ColorPickerPart.Bar2); + var b = GetColorBar (cp, ColorPickerPart.Bar3); + var hex = GetTextField (cp, ColorPickerPart.Hex); + + Assert.Equal ("R:", r.Text); + Assert.Equal (2, r.TrianglePosition); + Assert.IsType (r); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.IsType (g); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.IsType (b); + Assert.Equal ("#000000", hex.Text); + + Assert.IsAssignableFrom (cp.Focused); + + cp.Focused.OnMouseEvent ( + new () + { + Flags = MouseFlags.Button1Pressed, + Position = new (3, 0) + }); + + cp.Draw (); + + Assert.Equal (3, r.TrianglePosition); + Assert.Equal ("#0F0000", hex.Text); + + cp.Focused.OnMouseEvent ( + new () + { + Flags = MouseFlags.Button1Pressed, + Position = new (4, 0) + }); + + cp.Draw (); + + Assert.Equal (4, r.TrianglePosition); + Assert.Equal ("#1E0000", hex.Text); + + Application.Current?.Dispose (); + } + + + public static IEnumerable ColorPickerTestData () + { + yield return new object [] + { + new Color(255, 0), + "R:", 19, "G:", 2, "B:", 2, "#FF0000" + }; + + yield return new object [] + { + new Color(0, 255), + "R:", 2, "G:", 19, "B:", 2, "#00FF00" + }; + + yield return new object [] + { + new Color(0, 0, 255), + "R:", 2, "G:", 2, "B:", 19, "#0000FF" + }; + + yield return new object [] + { + new Color(125, 125, 125), + "R:", 11, "G:", 11, "B:", 11, "#7D7D7D" + }; + } + + [Theory] + [SetupFakeDriver] + [MemberData (nameof (ColorPickerTestData))] + public void ColorPicker_RGB_NoText (Color c, string expectedR, int expectedRTriangle, string expectedG, int expectedGTriangle, string expectedB, int expectedBTriangle, string expectedHex) + { + var cp = GetColorPicker (ColorModel.RGB, false); + cp.SelectedColor = c; + + cp.Draw (); + + var r = GetColorBar (cp, ColorPickerPart.Bar1); + var g = GetColorBar (cp, ColorPickerPart.Bar2); + var b = GetColorBar (cp, ColorPickerPart.Bar3); + var hex = GetTextField (cp, ColorPickerPart.Hex); + + Assert.Equal (expectedR, r.Text); + Assert.Equal (expectedRTriangle, r.TrianglePosition); + Assert.Equal (expectedG, g.Text); + Assert.Equal (expectedGTriangle, g.TrianglePosition); + Assert.Equal (expectedB, b.Text); + Assert.Equal (expectedBTriangle, b.TrianglePosition); + Assert.Equal (expectedHex, hex.Text); + + Application.Current.Dispose (); + } + + public static IEnumerable ColorPickerTestData_WithTextFields () + { + yield return new object [] + { + new Color(255, 0), + "R:", 15, 255, "G:", 2, 0, "B:", 2, 0, "#FF0000" + }; + + yield return new object [] + { + new Color(0, 255), + "R:", 2, 0, "G:", 15, 255, "B:", 2, 0, "#00FF00" + }; + + yield return new object [] + { + new Color(0, 0, 255), + "R:", 2, 0, "G:", 2, 0, "B:", 15, 255, "#0000FF" + }; + + yield return new object [] + { + new Color(125, 125, 125), + "R:", 9, 125, "G:", 9, 125, "B:", 9, 125, "#7D7D7D" + }; + } + + [Theory] + [SetupFakeDriver] + [MemberData (nameof (ColorPickerTestData_WithTextFields))] + public void ColorPicker_RGB_NoText_WithTextFields (Color c, string expectedR, int expectedRTriangle, int expectedRValue, string expectedG, int expectedGTriangle, int expectedGValue, string expectedB, int expectedBTriangle, int expectedBValue, string expectedHex) + { + var cp = GetColorPicker (ColorModel.RGB, true); + cp.SelectedColor = c; + + cp.Draw (); + + var r = GetColorBar (cp, ColorPickerPart.Bar1); + var g = GetColorBar (cp, ColorPickerPart.Bar2); + var b = GetColorBar (cp, ColorPickerPart.Bar3); + var hex = GetTextField (cp, ColorPickerPart.Hex); + var rTextField = GetTextField (cp, ColorPickerPart.Bar1); + var gTextField = GetTextField (cp, ColorPickerPart.Bar2); + var bTextField = GetTextField (cp, ColorPickerPart.Bar3); + + Assert.Equal (expectedR, r.Text); + Assert.Equal (expectedRTriangle, r.TrianglePosition); + Assert.Equal (expectedRValue.ToString (), rTextField.Text); + Assert.Equal (expectedG, g.Text); + Assert.Equal (expectedGTriangle, g.TrianglePosition); + Assert.Equal (expectedGValue.ToString (), gTextField.Text); + Assert.Equal (expectedB, b.Text); + Assert.Equal (expectedBTriangle, b.TrianglePosition); + Assert.Equal (expectedBValue.ToString (), bTextField.Text); + Assert.Equal (expectedHex, hex.Text); + + Application.Current?.Dispose (); } [Fact] - public void SelectedColorAndCursor () + [SetupFakeDriver] + public void ColorPicker_ClickingAtEndOfBar_SetsMaxValue () { - var colorPicker = new ColorPicker (); - colorPicker.SelectedColor = ColorName.White; - Assert.Equal (7, colorPicker.Cursor.X); - Assert.Equal (1, colorPicker.Cursor.Y); + var cp = GetColorPicker (ColorModel.RGB, false); - colorPicker.SelectedColor = Color.Black; - Assert.Equal (0, colorPicker.Cursor.X); - Assert.Equal (0, colorPicker.Cursor.Y); + cp.Draw (); - colorPicker.Cursor = new (7, 1); - Assert.Equal (ColorName.White, colorPicker.SelectedColor); + // Click at the end of the Red bar + cp.Focused.OnMouseEvent ( + new () + { + Flags = MouseFlags.Button1Pressed, + Position = new (19, 0) // Assuming 0-based indexing + }); - colorPicker.Cursor = Point.Empty; - Assert.Equal (ColorName.Black, colorPicker.SelectedColor); + cp.Draw (); + + var r = GetColorBar (cp, ColorPickerPart.Bar1); + var g = GetColorBar (cp, ColorPickerPart.Bar2); + var b = GetColorBar (cp, ColorPickerPart.Bar3); + var hex = GetTextField (cp, ColorPickerPart.Hex); + + Assert.Equal ("R:", r.Text); + Assert.Equal (19, r.TrianglePosition); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.Equal ("#FF0000", hex.Text); + + Application.Current?.Dispose (); + } + + [Fact] + [SetupFakeDriver] + public void ColorPicker_ClickingBeyondBar_ChangesToMaxValue () + { + var cp = GetColorPicker (ColorModel.RGB, false); + + cp.Draw (); + + // Click beyond the bar + cp.Focused.OnMouseEvent ( + new () + { + Flags = MouseFlags.Button1Pressed, + Position = new (21, 0) // Beyond the bar + }); + + cp.Draw (); + + var r = GetColorBar (cp, ColorPickerPart.Bar1); + var g = GetColorBar (cp, ColorPickerPart.Bar2); + var b = GetColorBar (cp, ColorPickerPart.Bar3); + var hex = GetTextField (cp, ColorPickerPart.Hex); + + Assert.Equal ("R:", r.Text); + Assert.Equal (19, r.TrianglePosition); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.Equal ("#FF0000", hex.Text); + + Application.Current?.Dispose (); + } + + [Fact] + [SetupFakeDriver] + public void ColorPicker_ChangeValueOnUI_UpdatesAllUIElements () + { + var cp = GetColorPicker (ColorModel.RGB, true); + + View otherView = new View () { CanFocus = true }; + + Application.Current?.Add (otherView); + + cp.Draw (); + + // Change value using text field + TextField rBarTextField = cp.Subviews.OfType ().First (tf => tf.Text == "0"); + + rBarTextField.Text = "128"; + //rBarTextField.OnLeave (cp); // OnLeave should be protected virtual. Don't call it. + otherView.SetFocus (); // Remove focus from the color picker + + cp.Draw (); + + var r = GetColorBar (cp, ColorPickerPart.Bar1); + var g = GetColorBar (cp, ColorPickerPart.Bar2); + var b = GetColorBar (cp, ColorPickerPart.Bar3); + var hex = GetTextField (cp, ColorPickerPart.Hex); + var rTextField = GetTextField (cp, ColorPickerPart.Bar1); + var gTextField = GetTextField (cp, ColorPickerPart.Bar2); + var bTextField = GetTextField (cp, ColorPickerPart.Bar3); + + Assert.Equal ("R:", r.Text); + Assert.Equal (9, r.TrianglePosition); + Assert.Equal ("128", rTextField.Text); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.Equal ("0", gTextField.Text); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.Equal ("0", bTextField.Text); + Assert.Equal ("#800000", hex.Text); + + Application.Current?.Dispose (); + } + + [Fact] + [SetupFakeDriver] + public void ColorPicker_InvalidHexInput_DoesNotChangeColor () + { + var cp = GetColorPicker (ColorModel.RGB, true); + + cp.Draw (); + + // Enter invalid hex value + TextField hexField = cp.Subviews.OfType ().First (tf => tf.Text == "#000000"); + hexField.Text = "#ZZZZZZ"; + hexField.OnLeave (cp); + + cp.Draw (); + + var r = GetColorBar (cp, ColorPickerPart.Bar1); + var g = GetColorBar (cp, ColorPickerPart.Bar2); + var b = GetColorBar (cp, ColorPickerPart.Bar3); + var hex = GetTextField (cp, ColorPickerPart.Hex); + + Assert.Equal ("R:", r.Text); + Assert.Equal (2, r.TrianglePosition); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.Equal ("#000000", hex.Text); + + Application.Current?.Dispose (); + } + + [Fact] + [SetupFakeDriver] + public void ColorPicker_ClickingDifferentBars_ChangesFocus () + { + var cp = GetColorPicker (ColorModel.RGB, false); + + cp.Draw (); + + // Click on Green bar + cp.Subviews.OfType () + .Single () + .OnMouseEvent ( + new () + { + Flags = MouseFlags.Button1Pressed, + Position = new (0, 1) + }); + + cp.Draw (); + + Assert.IsAssignableFrom (cp.Focused); + + // Click on Blue bar + cp.Subviews.OfType () + .Single () + .OnMouseEvent ( + new () + { + Flags = MouseFlags.Button1Pressed, + Position = new (0, 2) + }); + + cp.Draw (); + + Assert.IsAssignableFrom (cp.Focused); + + Application.Current?.Dispose (); + } + + [Fact] + [SetupFakeDriver] + public void ColorPicker_SwitchingColorModels_ResetsBars () + { + var cp = GetColorPicker (ColorModel.RGB, false); + cp.SelectedColor = new (255, 0); + + cp.Draw (); + + var r = GetColorBar (cp, ColorPickerPart.Bar1); + var g = GetColorBar (cp, ColorPickerPart.Bar2); + var b = GetColorBar (cp, ColorPickerPart.Bar3); + var hex = GetTextField (cp, ColorPickerPart.Hex); + + Assert.Equal ("R:", r.Text); + Assert.Equal (19, r.TrianglePosition); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.Equal ("#FF0000", hex.Text); + + // Switch to HSV + cp.Style.ColorModel = ColorModel.HSV; + cp.ApplyStyleChanges (); + + cp.Draw (); + + var h = GetColorBar (cp, ColorPickerPart.Bar1); + var s = GetColorBar (cp, ColorPickerPart.Bar2); + var v = GetColorBar (cp, ColorPickerPart.Bar3); + + Assert.Equal ("H:", h.Text); + Assert.Equal (2, h.TrianglePosition); + Assert.Equal ("S:", s.Text); + Assert.Equal (19, s.TrianglePosition); + Assert.Equal ("V:", v.Text); + Assert.Equal (19, v.TrianglePosition); + Assert.Equal ("#FF0000", hex.Text); + + Application.Current?.Dispose (); + } + + [Fact] + [SetupFakeDriver] + public void ColorPicker_SyncBetweenTextFieldAndBars () + { + var cp = GetColorPicker (ColorModel.RGB, true); + + cp.Draw (); + + // Change value using the bar + RBar rBar = cp.Subviews.OfType ().First (); + rBar.Value = 128; + + cp.Draw (); + + var r = GetColorBar (cp, ColorPickerPart.Bar1); + var g = GetColorBar (cp, ColorPickerPart.Bar2); + var b = GetColorBar (cp, ColorPickerPart.Bar3); + var hex = GetTextField (cp, ColorPickerPart.Hex); + var rTextField = GetTextField (cp, ColorPickerPart.Bar1); + var gTextField = GetTextField (cp, ColorPickerPart.Bar2); + var bTextField = GetTextField (cp, ColorPickerPart.Bar3); + + Assert.Equal ("R:", r.Text); + Assert.Equal (9, r.TrianglePosition); + Assert.Equal ("128", rTextField.Text); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.Equal ("0", gTextField.Text); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.Equal ("0", bTextField.Text); + Assert.Equal ("#800000", hex.Text); + + Application.Current?.Dispose (); + } + + enum ColorPickerPart + { + Bar1 = 0, + Bar2 = 1, + Bar3 = 2, + ColorName = 3, + Hex = 4, + } + + private TextField GetTextField (ColorPicker cp, ColorPickerPart toGet) + { + var hasBarValueTextFields = cp.Style.ShowTextFields; + var hasColorNameTextField = cp.Style.ShowColorName; + + switch (toGet) + { + case ColorPickerPart.Bar1: + case ColorPickerPart.Bar2: + case ColorPickerPart.Bar3: + if (!hasBarValueTextFields) + { + throw new NotSupportedException ("Corresponding Style option is not enabled"); + } + + return cp.Subviews.OfType ().ElementAt ((int)toGet); + case ColorPickerPart.ColorName: + if (!hasColorNameTextField) + { + throw new NotSupportedException ("Corresponding Style option is not enabled"); + } + + return cp.Subviews.OfType ().ElementAt (hasBarValueTextFields ? (int)toGet : (int)toGet -3); + case ColorPickerPart.Hex: + + int offset = hasBarValueTextFields ? 0 : 3; + offset += hasColorNameTextField ? 0 : 1; + + return cp.Subviews.OfType ().ElementAt ((int)toGet - offset); + default: + throw new ArgumentOutOfRangeException (nameof (toGet), toGet, null); + } + } + + private ColorBar GetColorBar (ColorPicker cp, ColorPickerPart toGet) + { + if (toGet <= ColorPickerPart.Bar3) + { + return cp.Subviews.OfType ().ElementAt ((int)toGet); + } + throw new NotSupportedException ("ColorPickerPart must be a bar"); + } + + [Fact] + [SetupFakeDriver] + public void ColorPicker_ChangedEvent_Fires () + { + Color newColor = default; + var count = 0; + + var cp = new ColorPicker (); + + cp.ColorChanged += (s, e) => + { + count++; + newColor = e.CurrentValue; + + Assert.Equal (cp.SelectedColor, e.CurrentValue); + }; + + cp.SelectedColor = new (1, 2, 3); + Assert.Equal (1, count); + Assert.Equal (new (1, 2, 3), newColor); + + cp.SelectedColor = new (2, 3, 4); + + Assert.Equal (2, count); + Assert.Equal (new (2, 3, 4), newColor); + + // Set to same value + cp.SelectedColor = new (2, 3, 4); + + // Should have no effect + Assert.Equal (2, count); + } + + [Fact] + [SetupFakeDriver] + public void ColorPicker_DisposesOldViews_OnModelChange () + { + var cp = GetColorPicker (ColorModel.HSL,true); + + var b1 = GetColorBar (cp, ColorPickerPart.Bar1); + var b2 = GetColorBar (cp, ColorPickerPart.Bar2); + var b3 = GetColorBar (cp, ColorPickerPart.Bar3); + + var tf1 = GetTextField (cp, ColorPickerPart.Bar1); + var tf2 = GetTextField (cp, ColorPickerPart.Bar2); + var tf3 = GetTextField (cp, ColorPickerPart.Bar3); + + var hex = GetTextField (cp, ColorPickerPart.Hex); + + Assert.All (new View [] { b1, b2, b3, tf1, tf2, tf3,hex }, b => Assert.False (b.WasDisposed)); + + cp.Style.ColorModel = ColorModel.RGB; + cp.ApplyStyleChanges (); + + var b1After = GetColorBar (cp, ColorPickerPart.Bar1); + var b2After = GetColorBar (cp, ColorPickerPart.Bar2); + var b3After = GetColorBar (cp, ColorPickerPart.Bar3); + + var tf1After = GetTextField (cp, ColorPickerPart.Bar1); + var tf2After = GetTextField (cp, ColorPickerPart.Bar2); + var tf3After = GetTextField (cp, ColorPickerPart.Bar3); + + var hexAfter = GetTextField (cp, ColorPickerPart.Hex); + + // Old bars should be disposed + Assert.All (new View [] { b1, b2, b3, tf1, tf2, tf3,hex }, b => Assert.True (b.WasDisposed)); + Assert.NotSame (hex,hexAfter); + + Assert.NotSame (b1,b1After); + Assert.NotSame (b2, b2After); + Assert.NotSame (b3, b3After); + + Assert.NotSame (tf1, tf1After); + Assert.NotSame (tf2, tf2After); + Assert.NotSame (tf3, tf3After); + + } + + [Fact] + [SetupFakeDriver] + public void ColorPicker_TabCompleteColorName() + { + var cp = GetColorPicker (ColorModel.RGB, true,true); + cp.Draw (); + + var r = GetColorBar (cp, ColorPickerPart.Bar1); + var g = GetColorBar (cp, ColorPickerPart.Bar2); + var b = GetColorBar (cp, ColorPickerPart.Bar3); + var name = GetTextField (cp, ColorPickerPart.ColorName); + var hex = GetTextField (cp, ColorPickerPart.Hex); + + name.FocusFirst (TabBehavior.TabStop); + + Assert.True (name.HasFocus); + Assert.Same (name, cp.Focused); + + name.Text = ""; + Assert.Empty (name.Text); + + Application.OnKeyDown (Key.A); + Application.OnKeyDown (Key.Q); + + Assert.Equal ("aq",name.Text); + + + // Auto complete the color name + Application.OnKeyDown (Key.Tab); + + Assert.Equal ("Aquamarine", name.Text); + + // Tab out of the text field + Application.OnKeyDown (Key.Tab); + + Assert.False (name.HasFocus); + Assert.NotSame (name, cp.Focused); + + Assert.Equal ("#7FFFD4", hex.Text); + + Application.Current?.Dispose (); + } + + [Fact] + [SetupFakeDriver] + public void ColorPicker_EnterHexFor_ColorName () + { + var cp = GetColorPicker (ColorModel.RGB, true, true); + cp.Draw (); + + var name = GetTextField (cp, ColorPickerPart.ColorName); + var hex = GetTextField (cp, ColorPickerPart.Hex); + + hex.FocusFirst (TabBehavior.TabStop); + + Assert.True (hex.HasFocus); + Assert.Same (hex, cp.Focused); + + hex.Text = ""; + name.Text = ""; + + Assert.Empty (hex.Text); + Assert.Empty (name.Text); + + Application.OnKeyDown ('#'); + Assert.Empty (name.Text); + //7FFFD4 + + Assert.Equal ("#", hex.Text); + Application.OnKeyDown ('7'); + Application.OnKeyDown ('F'); + Application.OnKeyDown ('F'); + Application.OnKeyDown ('F'); + Application.OnKeyDown ('D'); + Assert.Empty (name.Text); + + Application.OnKeyDown ('4'); + + // Tab out of the text field + Application.OnKeyDown (Key.Tab); + Assert.False (hex.HasFocus); + Assert.NotSame (hex, cp.Focused); + + // Color name should be recognised as a known string and populated + Assert.Equal ("#7FFFD4", hex.Text); + Assert.Equal("Aquamarine", name.Text); + + Application.Current?.Dispose (); + } + + [Fact] + public void TestColorNames () + { + var colors = new W3CColors (); + Assert.Contains ("Aquamarine", colors.GetColorNames ()); + Assert.DoesNotContain ("Save as",colors.GetColorNames ()); + } + private ColorPicker GetColorPicker (ColorModel colorModel, bool showTextFields, bool showName = false) + { + var cp = new ColorPicker { Width = 20, SelectedColor = new (0, 0) }; + cp.Style.ColorModel = colorModel; + cp.Style.ShowTextFields = showTextFields; + cp.Style.ShowColorName = showName; + cp.ApplyStyleChanges (); + + Application.Current = new Toplevel () { Width = 20 ,Height = 5}; + Application.Current.Add (cp); + Application.Current.FocusFirst (null); + + Application.Current.LayoutSubviews (); + + Application.Current.FocusFirst (null); + return cp; } }