using System.Collections.Frozen; using System.Globalization; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.Json.Serialization; using ColorHelper; using ColorConverter = ColorHelper.ColorConverter; namespace Terminal.Gui.Drawing; /// /// Represents a 24-bit color encoded in ARGB32 format. /// /// The RGB components define the color identity (what color it is), while the alpha channel defines /// rendering intent (how transparent it should be when drawn). /// /// /// /// /// When matching colors to standard color names (e.g., via ), /// the alpha channel is ignored. This means colors with the same RGB values but different alpha values /// will resolve to the same color name. This design supports transparency features while maintaining /// semantic color identity. /// /// /// While Terminal.Gui does not currently support alpha blending during rendering, the alpha channel /// is used to indicate rendering intent: /// /// Alpha = 0: Fully transparent (don't render) /// Alpha = 255: Fully opaque (normal rendering) /// Other values: Reserved for future alpha blending support /// /// /// /// /// /// [JsonConverter (typeof (ColorJsonConverter))] [StructLayout (LayoutKind.Explicit)] public readonly partial record struct Color : ISpanParsable, IUtf8SpanParsable, ISpanFormattable, IUtf8SpanFormattable, IMinMaxValue { /// The value of the alpha channel component /// /// /// The alpha channel represents rendering intent (transparency) rather than color identity. /// Terminal.Gui does not currently perform alpha blending, but uses this value to determine /// whether to render the color at all (alpha = 0 means don't render). /// /// /// When matching colors to standard color names, the alpha channel is ignored. For example, /// new Color(255, 0, 0, 255) and new Color(255, 0, 0, 128) will both be /// identified as "Red". /// /// [JsonIgnore] [field: FieldOffset (3)] public readonly byte A; /// The value of this as a in ARGB32 format. /// /// The alpha channel in the ARGB value represents rendering intent (transparency), not color identity. /// When matching to standard color names, only the RGB components are considered. /// [JsonIgnore] [field: FieldOffset (0)] public readonly uint Argb; /// The value of the blue color component. [JsonIgnore] [field: FieldOffset (0)] public readonly byte B; /// The value of the green color component. [JsonIgnore] [field: FieldOffset (1)] public readonly byte G; /// The value of the red color component. [JsonIgnore] [field: FieldOffset (2)] public readonly byte R; /// The value of this encoded as a signed 32-bit integer in ARGB32 format. [JsonIgnore] [field: FieldOffset (0)] public readonly int Rgba; /// /// Initializes a new instance of the using the supplied component /// values. /// /// The red 8-bits. /// The green 8-bits. /// The blue 8-bits. /// Optional; defaults to 0xFF. The Alpha channel is not supported by Terminal.Gui. /// Alpha channel is not currently supported by Terminal.Gui. /// If the value of any parameter is greater than . /// If the value of any parameter is negative. public Color (int red = 0, int green = 0, int blue = 0, int alpha = byte.MaxValue) { ArgumentOutOfRangeException.ThrowIfNegative (red, nameof (red)); ArgumentOutOfRangeException.ThrowIfNegative (green, nameof (green)); ArgumentOutOfRangeException.ThrowIfNegative (blue, nameof (blue)); ArgumentOutOfRangeException.ThrowIfNegative (alpha, nameof (alpha)); A = Convert.ToByte (alpha); R = Convert.ToByte (red); G = Convert.ToByte (green); B = Convert.ToByte (blue); } /// /// Initializes a new instance of the class with an encoded signed 32-bit color value in /// ARGB32 format. /// /// The encoded 32-bit color value (see ). /// /// The alpha channel is not currently supported, so the value of the alpha channel bits will not affect /// rendering. /// public Color (int rgba) { Rgba = rgba; } /// /// Initializes a new instance of the class with an encoded unsigned 32-bit color value in /// ARGB32 format. /// /// The encoded unsigned 32-bit color value (see ). /// /// The alpha channel is not currently supported, so the value of the alpha channel bits will not affect /// rendering. /// public Color (uint argb) { Argb = argb; } /// Initializes a new instance of the color from a legacy 16-color named value. /// The 16-color value. public Color (in ColorName16 colorName) { this = ColorExtensions.ColorName16ToColorMap! [colorName]; } /// Initializes a new instance of the color from a value in the enum. /// The 16-color value. public Color (in StandardColor colorName) : this (StandardColors.GetArgb (colorName)) { } /// /// Initializes a new instance of the color from string. See /// for details. /// /// /// If is . /// /// If is an empty string or consists of only whitespace /// characters. /// /// If thrown by public Color (string colorString) { ArgumentException.ThrowIfNullOrWhiteSpace (colorString, nameof (colorString)); this = Parse (colorString, CultureInfo.InvariantCulture); } /// Initializes a new instance of the with all channels set to 0. public Color () { Argb = 0u; } /// Gets or sets the 3-byte/6-character hexadecimal value for each of the legacy 16-color values. [ConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)] public static Dictionary Colors16 { get => // Transform _colorToNameMap into a Dictionary ColorExtensions.ColorToName16Map!.ToDictionary (static kvp => kvp.Value, static kvp => kvp.Key.ToString ("g")); set { // Transform Dictionary into _colorToNameMap ColorExtensions.ColorToName16Map = value.ToFrozenDictionary (GetColorToNameMapKey, GetColorToNameMapValue); return; static Color GetColorToNameMapKey (KeyValuePair kvp) { return new (kvp.Value); } static ColorName16 GetColorToNameMapValue (KeyValuePair kvp) { return Enum.TryParse (kvp.Key.ToString (), true, out ColorName16 colorName) ? colorName : throw new ArgumentException ($"Invalid color name: {kvp.Key}"); } } } /// /// Gets the using a legacy 16-color value. will /// return the closest 16 color match to the true color when no exact value is found. /// /// /// Get returns the of the closest 24-bit color value. Set sets the RGB /// value using a hard-coded map. /// public AnsiColorCode GetAnsiColorCode () { return ColorExtensions.ColorName16ToAnsiColorMap [GetClosestNamedColor16 ()]; } /// /// Gets the using a legacy 16-color value. /// will return the closest 16 color match to the true color when no exact value is found. /// /// /// Get returns the of the closest 24-bit color value. Set /// sets the RGB /// value using a hard-coded map. /// public ColorName16 GetClosestNamedColor16 () { return GetClosestNamedColor16 (this); } /// /// Determines if the closest named to is the provided /// . /// /// /// The to check if this is closer /// to than any other configured named color. /// /// /// if the closest named color is the provided value.
if any /// other named color is closer to this than . ///
/// /// If is equidistant from two named colors, the result of this method is not guaranteed to /// be determinate. /// [Pure] [MethodImpl (MethodImplOptions.AggressiveInlining)] public bool IsClosestToNamedColor16 (in ColorName16 namedColor) { return GetClosestNamedColor16 () == namedColor; } /// Gets the "closest" named color to this value. /// /// /// Distance is defined here as the Euclidean distance between each color interpreted as a . /// /// [SkipLocalsInit] internal static ColorName16 GetClosestNamedColor16 (Color inputColor) { return ColorExtensions.ColorToName16Map!.MinBy (pair => CalculateColorDistance (inputColor, pair.Key)).Value; } [SkipLocalsInit] private static float CalculateColorDistance (in Vector4 color1, in Vector4 color2) { return Vector4.Distance (color1, color2); } /// /// Returns a color with the same hue and saturation as this color, but with a significantly different lightness, /// making it suitable for use as a highlight or contrast color in UI elements. /// /// /// /// This method brightens the color if it is dark, or darkens it if it is light, ensuring the result is visually /// distinct /// from the original. The algorithm works in HSL color space and adjusts the lightness channel: /// /// /// If the color is dark (lightness < 0.5), the lightness is increased (brightened). /// /// /// If the color is light (lightness >= 0.5), the lightness is decreased (darkened). /// /// /// /// If the adjustment resulted in a color too close to the original, a larger adjustment is /// made. /// /// /// /// This ensures the returned color is always visually distinct and suitable for highlighting or selection states. /// /// /// The returned color will always have the same hue and saturation as the original, but a different lightness. /// /// /// The percent amount to brighten the color by. The default is 20%. /// /// A instance with the same hue and saturation as this color, but with a contrasting lightness. /// /// /// /// var baseColor = new Color(100, 100, 100); /// var highlight = baseColor.GetHighlightColor(); /// // highlight will be a lighter or darker version of baseColor, depending on its original lightness. /// /// public Color GetBrighterColor (double brightenAmount = 0.2) { HSL? hsl = ColorConverter.RgbToHsl (new (R, G, B)); double lNorm = hsl.L / 255.0; double newL = lNorm < 0.5 ? Math.Min (1.0, lNorm + brightenAmount) : Math.Max (0.0, lNorm - brightenAmount); if (Math.Abs (newL - lNorm) < 0.1) { newL = lNorm < 0.5 ? Math.Min (1.0, lNorm + 2 * brightenAmount) : Math.Max (0.0, lNorm - 2 * brightenAmount); } var newHsl = new HSL (hsl.H, hsl.S, (byte)(newL * 255)); RGB? rgb = ColorConverter.HslToRgb (newHsl); return new (rgb.R, rgb.G, rgb.B); } /// /// Returns a color with the same hue and saturation as this color, but with a significantly lower lightness, /// making it suitable for use as a shadow or background contrast color in UI elements. /// /// /// /// This method darkens the color by reducing its lightness in HSL color space: /// /// /// If the color is already very dark, returns . /// /// /// Otherwise, reduces the lightness by a fixed amount (default 30%). /// /// /// /// If the adjustment resulted in a color too close to the original, a larger adjustment is /// made. /// /// /// /// This ensures the returned color is always visually distinct and suitable for shadowing or de-emphasis. /// /// /// The percent amount to dim the color by. The default is 20%. /// /// A instance with the same hue and saturation as this color, but with a much lower lightness. /// public Color GetDimColor (double dimAmount = 0.2) { HSL hsl = ColorConverter.RgbToHsl (new (R, G, B)); double lNorm = hsl.L / 255.0; double newL = Math.Max (0.0, lNorm - dimAmount); // If the color is already very dark, return a standard dark gray for visibility if (lNorm <= 0.1) { return new (ColorName16.DarkGray); } // If the new lightness is too close to the original, force a bigger change if (Math.Abs (newL - lNorm) < 0.1) { newL = Math.Max (0.0, lNorm - 2 * dimAmount); } var newHsl = new HSL (hsl.H, hsl.S, (byte)(newL * 255)); RGB rgb = ColorConverter.HslToRgb (newHsl); return new (rgb.R, rgb.G, rgb.B); } #region Legacy Color Names // ReSharper disable InconsistentNaming /// The black color. public const ColorName16 Black = ColorName16.Black; /// The blue color. public const ColorName16 Blue = ColorName16.Blue; /// The green color. public const ColorName16 Green = ColorName16.Green; /// The cyan color. public const ColorName16 Cyan = ColorName16.Cyan; /// The red color. public const ColorName16 Red = ColorName16.Red; /// The magenta color. public const ColorName16 Magenta = ColorName16.Magenta; /// The yellow color. public const ColorName16 Yellow = ColorName16.Yellow; /// The gray color. public const ColorName16 Gray = ColorName16.Gray; /// The dark gray color. public const ColorName16 DarkGray = ColorName16.DarkGray; /// The bright bBlue color. public const ColorName16 BrightBlue = ColorName16.BrightBlue; /// The bright green color. public const ColorName16 BrightGreen = ColorName16.BrightGreen; /// The bright cyan color. public const ColorName16 BrightCyan = ColorName16.BrightCyan; /// The bright red color. public const ColorName16 BrightRed = ColorName16.BrightRed; /// The bright magenta color. public const ColorName16 BrightMagenta = ColorName16.BrightMagenta; /// The bright yellow color. public const ColorName16 BrightYellow = ColorName16.BrightYellow; /// The White color. public const ColorName16 White = ColorName16.White; #endregion }