diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 8c831fdb2c..63e86d01d4 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -958,12 +958,22 @@ public static bool Is_WSL_Platform () return false; } var result = BashRunner.Run ("uname -a", runCurses: false); - if (result.Contains ("microsoft") && result.Contains ("WSL")) { + if (result.ToLower ().Contains ("microsoft") && (Environment.GetEnvironmentVariable ("WSL_DISTRO_NAME") != null)) { return true; } return false; } + public static bool CanColorTermTrueColor () + { + if (Environment.GetEnvironmentVariable ("COLORTERM") is string value) { + value = value.ToLower (); + return value.Contains ("truecolor") || value.Contains ("24bit"); + } + + return false; + } + static int MapColor (Color color) { switch (color) { diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index e1d85a8fce..c2ba8e1115 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -18,6 +18,8 @@ internal class NetWinVTConsole { IntPtr InputHandle, OutputHandle, ErrorHandle; uint originalInputConsoleMode, originalOutputConsoleMode, originalErrorConsoleMode; + public bool SupportTrueColor { get; } = (Environment.OSVersion.Version.Build >= 14931); + public NetWinVTConsole () { InputHandle = GetStdHandle (STD_INPUT_HANDLE); @@ -1162,6 +1164,9 @@ public struct InputResult { } internal class NetDriver : ConsoleDriver { + + Attribute [] OutputAttributeBuffer; + const int COLOR_BLACK = 30; const int COLOR_RED = 31; const int COLOR_GREEN = 32; @@ -1192,6 +1197,9 @@ internal class NetDriver : ConsoleDriver { public override IClipboard Clipboard { get; } public override int [,,] Contents => contents; + readonly bool supportsTrueColorOutput; + public override bool SupportsTrueColorOutput => supportsTrueColorOutput; + int largestWindowHeight; public NetDriver () @@ -1200,6 +1208,7 @@ public NetDriver () if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { IsWinPlatform = true; NetWinConsole = new NetWinVTConsole (); + supportsTrueColorOutput = NetWinConsole.SupportTrueColor; } //largestWindowHeight = Math.Max (Console.BufferHeight, largestWindowHeight); largestWindowHeight = Console.BufferHeight; @@ -1213,7 +1222,9 @@ public NetDriver () } else { Clipboard = new CursesClipboard (); } + supportsTrueColorOutput = CursesDriver.CanColorTermTrueColor (); } + UseTrueColor = true; } // The format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag @@ -1239,6 +1250,7 @@ public override void AddRune (Rune rune) rune = MakePrintable (rune); var runeWidth = Rune.ColumnWidth (rune); var validClip = IsValidContent (ccol, crow, Clip); + var position = crow * Cols + ccol; if (validClip) { if (runeWidth < 2 && ccol > 0 @@ -1258,6 +1270,7 @@ public override void AddRune (Rune rune) } else { contents [crow, ccol, 0] = (int)(uint)rune; } + OutputAttributeBuffer [position] = currentAttribute; contents [crow, ccol, 1] = currentAttribute; contents [crow, ccol, 2] = 1; @@ -1267,6 +1280,7 @@ public override void AddRune (Rune rune) ccol++; if (runeWidth > 1) { if (validClip && ccol < Clip.Right) { + OutputAttributeBuffer [position] = currentAttribute; contents [crow, ccol, 1] = currentAttribute; contents [crow, ccol, 2] = 0; } @@ -1391,6 +1405,9 @@ public override void ResizeScreen () $";{Rows};{Cols}w"); } } + + OutputAttributeBuffer = new Attribute [Rows * Cols]; + Clip = new Rect (0, 0, Cols, Rows); Console.Out.Write ("\x1b[3J"); Console.Out.Flush (); @@ -1406,6 +1423,9 @@ public override void UpdateOffScreen () try { for (int row = 0; row < rows; row++) { for (int c = 0; c < cols; c++) { + int position = row * cols + c; + OutputAttributeBuffer [position] = Colors.TopLevel.Normal; + contents [row, c, 0] = ' '; contents [row, c, 1] = (ushort)Colors.TopLevel.Normal; contents [row, c, 2] = 0; @@ -1427,7 +1447,7 @@ public override void Refresh () UpdateCursor (); } - int redrawAttr = -1; + Attribute redrawAttr = null; public override void UpdateScreen () { @@ -1478,7 +1498,7 @@ public override void UpdateScreen () if (lastCol == -1) lastCol = col; - var attr = contents [row, col, 1]; + var attr = OutputAttributeBuffer [row * cols + col]; if (attr != redrawAttr) { output.Append (WriteAttributes (attr)); } @@ -1502,24 +1522,42 @@ void SetVirtualCursorPosition (int lastCol, int row) Console.Out.Flush (); } - System.Text.StringBuilder WriteAttributes (int attr) + System.Text.StringBuilder WriteAttributes (Attribute attr) { - const string CSI = "\x1b["; - int bg = 0; - int fg = 0; System.Text.StringBuilder sb = new System.Text.StringBuilder (); - redrawAttr = attr; - IEnumerable values = Enum.GetValues (typeof (ConsoleColor)) - .OfType () - .Select (s => (int)s); - if (values.Contains (attr & 0xffff)) { - bg = MapColors ((ConsoleColor)(attr & 0xffff), false); - } - if (values.Contains ((attr >> 16) & 0xffff)) { - fg = MapColors ((ConsoleColor)((attr >> 16) & 0xffff)); + if ((UseTrueColor) && (attr is TrueColorAttribute tca)) { + sb.Append (new [] { '\x1b', '[', '3', '8', ';', '2', ';' }); + sb.Append (tca.TrueColorForeground.Red); + sb.Append (';'); + sb.Append (tca.TrueColorForeground.Green); + sb.Append (';'); + sb.Append (tca.TrueColorForeground.Blue); + sb.Append (new [] { ';', '4', '8', ';', '2', ';' }); + sb.Append (tca.TrueColorBackground.Red); + sb.Append (';'); + sb.Append (tca.TrueColorBackground.Green); + sb.Append (';'); + sb.Append (tca.TrueColorBackground.Blue); + sb.Append ('m'); + } else { + const string CSI = "\x1b["; + int bg = 0; + int fg = 0; + + IEnumerable values = Enum.GetValues (typeof (ConsoleColor)) + .OfType () + .Select (s => (int)s); + if (values.Contains (attr & 0xffff)) { + bg = MapColors ((ConsoleColor)(attr & 0xffff), false); + } + if (values.Contains ((attr >> 16) & 0xffff)) { + fg = MapColors ((ConsoleColor)((attr >> 16) & 0xffff)); + } + sb.Append ($"{CSI}{bg};{fg}m"); } - sb.Append ($"{CSI}{bg};{fg}m"); + + redrawAttr = attr; return sb; } diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 66eeed296c..a52e08f7d1 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -58,17 +58,90 @@ public WindowsConsole () newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode; newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput; ConsoleMode = newConsoleMode; + + uint newOutputConsoleMode = OutputConsoleMode; + newOutputConsoleMode |= (uint)(OutputConsoleModes.EnableProcessedOutput | OutputConsoleModes.EnableVirtualTerminalProcessing | OutputConsoleModes.DisableNewLineAutoReturn); + newOutputConsoleMode &= ~(uint)(OutputConsoleModes.EnableWrapAtEolOutput); + OutputConsoleMode = newOutputConsoleMode; } public CharInfo [] OriginalStdOutChars; - public bool WriteToConsole (Size size, CharInfo [] charInfoBuffer, Coord coords, SmallRect window) + public bool SupportTrueColor { get; } = (Environment.OSVersion.Version.Build >= 14931); + + public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord coords, SmallRect window, bool forceUseBasicColor) { if (ScreenBuffer == IntPtr.Zero) { ReadFromConsoleOutput (size, coords, ref window); } - return WriteConsoleOutput (ScreenBuffer, charInfoBuffer, coords, new Coord () { X = window.Left, Y = window.Top }, ref window); + if (!SupportTrueColor || forceUseBasicColor) { + var i = 0; + var ci = new CharInfo [charInfoBuffer.Length]; + foreach (var info in charInfoBuffer) { + ci[i++] = new CharInfo () { + Char = new CharUnion () { UnicodeChar = info.Char }, + Attributes = (ushort)(int)info.Attribute + }; + } + + return WriteConsoleOutput (ScreenBuffer, ci, coords, new Coord () { X = window.Left, Y = window.Top }, ref window); + } + + return WriteConsoleTrueColorOutput (charInfoBuffer); + } + + readonly System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder (256*1024); + readonly char [] SafeCursor = new [] { '\x1b', '7', '\x1b', '[', '0', ';', '0', 'H' }; + readonly char [] RestoreCursor = new [] { '\x1b', '8' }; + readonly char [] SendTrueColorFg = new [] { '\x1b', '[', '3', '8', ';', '2', ';' }; + readonly char [] SendTrueColorBg = new [] { ';', '4', '8', ';', '2', ';' }; + readonly char [] SendColorFg = new [] { '\x1b', '[', '3', '8', ';', '5', ';' }; + readonly char [] SendColorBg = new [] { ';', '4', '8', ';', '5', ';' }; + + private bool WriteConsoleTrueColorOutput(ExtendedCharInfo [] charInfoBuffer) + { + stringBuilder.Clear (); + + stringBuilder.Append (SafeCursor); + + Attribute prev = null; + foreach (var info in charInfoBuffer) { + var attr = info.Attribute; + + if (attr != prev) { + prev = attr; + if (attr is TrueColorAttribute tca) { + stringBuilder.Append (SendTrueColorFg); + stringBuilder.Append (tca.TrueColorForeground.Red); + stringBuilder.Append (';'); + stringBuilder.Append (tca.TrueColorForeground.Green); + stringBuilder.Append (';'); + stringBuilder.Append (tca.TrueColorForeground.Blue); + stringBuilder.Append (SendTrueColorBg); + stringBuilder.Append (tca.TrueColorBackground.Red); + stringBuilder.Append (';'); + stringBuilder.Append (tca.TrueColorBackground.Green); + stringBuilder.Append (';'); + stringBuilder.Append (tca.TrueColorBackground.Blue); + stringBuilder.Append ('m'); + } else { + var cc = (int)attr; + stringBuilder.Append (SendColorFg); + stringBuilder.Append (TrueColor.Code4ToCode8 (cc % 16)); + stringBuilder.Append (SendColorBg); + stringBuilder.Append (TrueColor.Code4ToCode8 (cc / 16)); + stringBuilder.Append ('m'); + } + } + + stringBuilder.Append (info.Char != '\x1b' ? info.Char : ' '); + } + + stringBuilder.Append (RestoreCursor); + + string s = stringBuilder.ToString (); + return WriteConsole (ScreenBuffer, s, (uint)(s.Length), out uint _, null); } public void ReadFromConsoleOutput (Size size, Coord coords, ref SmallRect window) @@ -302,14 +375,33 @@ public uint ConsoleMode { } } + public uint OutputConsoleMode { + get { + GetConsoleMode (OutputHandle, out uint v); + return v; + } + set { + SetConsoleMode (OutputHandle, value); + } + } + [Flags] public enum ConsoleModes : uint { EnableProcessedInput = 1, + EnableWindowInput = 8, EnableMouseInput = 16, EnableQuickEditMode = 64, EnableExtendedFlags = 128, } + [Flags] + public enum OutputConsoleModes : uint { + EnableProcessedOutput = 1, + EnableWrapAtEolOutput = 2, + EnableVirtualTerminalProcessing = 4, + DisableNewLineAutoReturn = 8, + } + [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] public struct KeyEventRecord { [FieldOffset (0), MarshalAs (UnmanagedType.Bool)] @@ -482,6 +574,11 @@ public struct CharInfo { [FieldOffset (2)] public ushort Attributes; } + public struct ExtendedCharInfo { + public char Char { get; set; } + public Attribute Attribute { get; set; } + } + [StructLayout (LayoutKind.Sequential)] public struct SmallRect { public short Left; @@ -576,6 +673,15 @@ static extern bool WriteConsoleOutput ( ref SmallRect lpWriteRegion ); + [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)] + static extern bool WriteConsole ( + IntPtr hConsoleOutput, + String lpbufer, + UInt32 NumberOfCharsToWriten, + out UInt32 lpNumberOfCharsWritten, + object lpReserved + ); + [DllImport ("kernel32.dll")] static extern bool SetConsoleCursorPosition (IntPtr hConsoleOutput, Coord dwCursorPosition); @@ -722,7 +828,7 @@ static extern Coord GetLargestConsoleWindowSize ( internal class WindowsDriver : ConsoleDriver { static bool sync = false; - WindowsConsole.CharInfo [] OutputBuffer; + WindowsConsole.ExtendedCharInfo [] OutputBuffer; int cols, rows, left, top; WindowsConsole.SmallRect damageRegion; IClipboard clipboard; @@ -736,6 +842,8 @@ internal class WindowsDriver : ConsoleDriver { public override IClipboard Clipboard => clipboard; public override int [,,] Contents => contents; + public override bool SupportsTrueColorOutput => WinConsole.SupportTrueColor; + public WindowsConsole WinConsole { get; private set; } Action keyHandler; @@ -747,6 +855,7 @@ public WindowsDriver () { WinConsole = new WindowsConsole (); clipboard = new WindowsClipboard (); + UseTrueColor = true; } public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action keyDownHandler, Action keyUpHandler, Action mouseHandler) @@ -1469,7 +1578,7 @@ public override void Init (Action terminalResized) public override void ResizeScreen () { - OutputBuffer = new WindowsConsole.CharInfo [Rows * Cols]; + OutputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols]; Clip = new Rect (0, 0, Cols, Rows); damageRegion = new WindowsConsole.SmallRect () { Top = 0, @@ -1488,10 +1597,10 @@ public override void UpdateOffScreen () for (int row = 0; row < rows; row++) { for (int col = 0; col < cols; col++) { int position = row * cols + col; - OutputBuffer [position].Attributes = (ushort)Colors.TopLevel.Normal; - OutputBuffer [position].Char.UnicodeChar = ' '; - contents [row, col, 0] = OutputBuffer [position].Char.UnicodeChar; - contents [row, col, 1] = OutputBuffer [position].Attributes; + OutputBuffer [position].Attribute = Colors.TopLevel.Normal; + OutputBuffer [position].Char = ' '; + contents [row, col, 0] = OutputBuffer [position].Char; + contents [row, col, 1] = OutputBuffer [position].Attribute; contents [row, col, 2] = 0; } } @@ -1521,25 +1630,25 @@ public override void AddRune (Rune rune) && Rune.ColumnWidth ((char)contents [crow, ccol - 1, 0]) > 1) { var prevPosition = crow * Cols + (ccol - 1); - OutputBuffer [prevPosition].Char.UnicodeChar = ' '; + OutputBuffer [prevPosition].Char = ' '; contents [crow, ccol - 1, 0] = (int)(uint)' '; } else if (runeWidth < 2 && ccol <= Clip.Right - 1 && Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) { var prevPosition = GetOutputBufferPosition () + 1; - OutputBuffer [prevPosition].Char.UnicodeChar = (char)' '; + OutputBuffer [prevPosition].Char = (char)' '; contents [crow, ccol + 1, 0] = (int)(uint)' '; } if (runeWidth > 1 && ccol == Clip.Right - 1) { - OutputBuffer [position].Char.UnicodeChar = (char)' '; + OutputBuffer [position].Char = (char)' '; contents [crow, ccol, 0] = (int)(uint)' '; } else { - OutputBuffer [position].Char.UnicodeChar = (char)rune; + OutputBuffer [position].Char = (char)rune; contents [crow, ccol, 0] = (int)(uint)rune; } - OutputBuffer [position].Attributes = (ushort)currentAttribute; + OutputBuffer [position].Attribute = currentAttribute; contents [crow, ccol, 1] = currentAttribute; contents [crow, ccol, 2] = 1; WindowsConsole.SmallRect.Update (ref damageRegion, (short)ccol, (short)crow); @@ -1549,8 +1658,8 @@ public override void AddRune (Rune rune) if (runeWidth > 1) { if (validClip && ccol < Clip.Right) { position = GetOutputBufferPosition (); - OutputBuffer [position].Attributes = (ushort)currentAttribute; - OutputBuffer [position].Char.UnicodeChar = (char)0x00; + OutputBuffer [position].Attribute = currentAttribute; + OutputBuffer [position].Char = (char)0x00; contents [crow, ccol, 0] = (int)(uint)0x00; contents [crow, ccol, 1] = currentAttribute; contents [crow, ccol, 2] = 0; @@ -1643,7 +1752,7 @@ public override void UpdateScreen () // Bottom = (short)Clip.Bottom //}; - WinConsole.WriteToConsole (new Size (Cols, Rows), OutputBuffer, bufferCoords, damageRegion); + WinConsole.WriteToConsole (new Size (Cols, Rows), OutputBuffer, bufferCoords, damageRegion, !UseTrueColor); // System.Diagnostics.Debugger.Log (0, "debug", $"Region={damageRegion.Right - damageRegion.Left},{damageRegion.Bottom - damageRegion.Top}\n"); WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); diff --git a/Terminal.Gui/Core/Border.cs b/Terminal.Gui/Core/Border.cs index 55d73a65c9..3e04613bf6 100644 --- a/Terminal.Gui/Core/Border.cs +++ b/Terminal.Gui/Core/Border.cs @@ -323,7 +323,7 @@ public override bool MouseEvent (MouseEvent mouseEvent) private Thickness padding; private bool effect3D; private Point effect3DOffset = new Point (1, 1); - private Attribute? effect3DBrush; + private Attribute effect3DBrush; private ustring title = ustring.Empty; /// @@ -469,7 +469,7 @@ public Point Effect3DOffset { /// /// Gets or sets the color for the /// - public Attribute? Effect3DBrush { + public Attribute Effect3DBrush { get => effect3DBrush; set { effect3DBrush = value; diff --git a/Terminal.Gui/Core/ConsoleDriver.cs b/Terminal.Gui/Core/ConsoleDriver.cs index 8586a288d3..6deaea8a8f 100644 --- a/Terminal.Gui/Core/ConsoleDriver.cs +++ b/Terminal.Gui/Core/ConsoleDriver.cs @@ -82,6 +82,85 @@ public enum Color { White } + /// + /// Represents a 24bit color. Supports translating to 4bit (Windows) and 8bit (Linux) colors. + /// + public struct TrueColor { + /// + /// Red color component. + /// + public int Red { get; } + /// + /// Green color component. + /// + public int Green { get; } + /// + /// Blue color component. + /// + public int Blue { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// + /// + public TrueColor (int red, int green, int blue) + { + Red = red; + Green = green; + Blue = blue; + } + + /// + /// Get color by 16 colors palette + /// + public static TrueColor Color4 (int code) + { + if (code == 7) { code = 8; } else if (code == 8) { code = 7; } + + if (code == 8) { return new TrueColor (192, 192, 192); } + + int k = (code > 8) ? 255 : 128; + + return new TrueColor (code / 4 % 2 * k, code / 2 % 2 * k, code % 2 * k); + } + + /// + /// Return color diff + /// + public static int Diff (TrueColor c1, TrueColor c2) + { + // TODO: maybe use CIEDE2000 + return ((c1.Red - c2.Red) * (c1.Red - c2.Red)) + + ((c1.Green - c2.Green) * (c1.Green - c2.Green)) + + (c1.Blue - c2.Blue) * (c1.Blue - c2.Blue); + } + + /// + /// Get color code in 16 colors palette (use approximation) + /// + public static int GetCode4 (TrueColor c) + { + int ans = 0; + for (int i = 1; i < 16; i++) { + if (Diff (Color4 (i), c) < Diff (Color4 (ans), c)) { ans = i; } + } + + return ans; + } + + /// + /// Convert code in 16 colors palette to 256 colors palette + /// + public static int Code4ToCode8 (int code) + { + if (code == 0 || code == 7 || code == 8 || code == 15) + return code; + return (code & 8) + (code & 2) + 4 * (code & 1) + (code & 4) / 4; + } + } + /// /// Attributes are used as elements that contain both a foreground and a background or platform specific features /// @@ -90,7 +169,7 @@ public enum Color { /// scenarios, they encode both the foreground and the background color and are used in the /// class to define color schemes that can be used in your application. /// - public struct Attribute { + public class Attribute : IEquatable { /// /// The color attribute value. /// @@ -105,11 +184,11 @@ public struct Attribute { public Color Background { get; } /// - /// Initializes a new instance of the struct with only the value passed to + /// Initializes a new instance of the class with only the value passed to /// and trying to get the colors if defined. /// /// Value. - public Attribute (int value) + public Attribute (int value = 0) { Color foreground = default; Color background = default; @@ -140,7 +219,7 @@ public Attribute (int value, Color foreground, Color background) /// /// Foreground /// Background - public Attribute (Color foreground = new Color (), Color background = new Color ()) + public Attribute (Color foreground, Color background) { Value = Make (foreground, background).Value; Foreground = foreground; @@ -154,12 +233,18 @@ public Attribute (int value, Color foreground, Color background) /// The color. public Attribute (Color color) : this (color, color) { } + /// + public bool Equals (Attribute other) + { + return (Value == other.Value) && (Foreground == other.Foreground) && (Background == other.Background); + } + /// /// Implicit conversion from an to the underlying Int32 representation /// /// The integer value stored in the attribute. /// The attribute to convert - public static implicit operator int (Attribute c) => c.Value; + public static implicit operator int (Attribute c) => c?.Value ?? 0; /// /// Implicitly convert an integer value into an @@ -191,6 +276,44 @@ public static Attribute Get () throw new InvalidOperationException ("The Application has not been initialized"); return Application.Driver.GetAttribute (); } + + /// + /// Default empty attribute. + /// + public static readonly Attribute Default = new Attribute (); + } + + /// + /// Defines a true color attribute. + /// + public class TrueColorAttribute : Attribute { + /// + /// Initializes a new instance of the struct. + /// + /// Foreground + /// Background + public TrueColorAttribute (TrueColor foreground, TrueColor background) + : base ((Color)TrueColor.GetCode4 (foreground), (Color)TrueColor.GetCode4 (background)) + { + TrueColorForeground = foreground; + TrueColorBackground = background; + } + + /// + /// Initializes a new instance of the struct + /// with the same colors for the foreground and background. + /// + /// The color. + public TrueColorAttribute (TrueColor color) : this (color, color) { } + + /// + /// The foreground color. + /// + public TrueColor TrueColorForeground { get; } + /// + /// The background color. + /// + public TrueColor TrueColorBackground { get; } } /// @@ -199,11 +322,11 @@ public static Attribute Get () /// views contained inside. /// public class ColorScheme : IEquatable { - Attribute _normal; - Attribute _focus; - Attribute _hotNormal; - Attribute _hotFocus; - Attribute _disabled; + Attribute _normal = Attribute.Default; + Attribute _focus = Attribute.Default; + Attribute _hotNormal = Attribute.Default; + Attribute _hotFocus = Attribute.Default; + Attribute _disabled = Attribute.Default; internal string caller = ""; /// @@ -246,18 +369,18 @@ Attribute SetAttribute (Attribute attribute, [CallerMemberName] string callerMem case "TopLevel": switch (callerMemberName) { case "Normal": - HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background); + HotNormal = Application.Driver.MakeAttribute (HotNormal?.Foreground ?? default, attribute.Background); break; case "Focus": - HotFocus = Application.Driver.MakeAttribute (HotFocus.Foreground, attribute.Background); + HotFocus = Application.Driver.MakeAttribute (HotFocus?.Foreground ?? default, attribute.Background); break; case "HotNormal": - HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus.Background); + HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus?.Background ?? default); break; case "HotFocus": - HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal.Background); - if (Focus.Foreground != attribute.Background) - Focus = Application.Driver.MakeAttribute (Focus.Foreground, attribute.Background); + HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal?.Background ?? default); + if (Focus?.Foreground != attribute.Background) + Focus = Application.Driver.MakeAttribute (Focus?.Foreground ?? default, attribute.Background); break; } break; @@ -265,19 +388,19 @@ Attribute SetAttribute (Attribute attribute, [CallerMemberName] string callerMem case "Base": switch (callerMemberName) { case "Normal": - HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background); + HotNormal = Application.Driver.MakeAttribute (HotNormal?.Foreground ?? default, attribute.Background); break; case "Focus": - HotFocus = Application.Driver.MakeAttribute (HotFocus.Foreground, attribute.Background); + HotFocus = Application.Driver.MakeAttribute (HotFocus?.Foreground ?? default, attribute.Background); break; case "HotNormal": - HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus.Background); - Normal = Application.Driver.MakeAttribute (Normal.Foreground, attribute.Background); + HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus?.Background ?? default); + Normal = Application.Driver.MakeAttribute (Normal?.Foreground ?? default, attribute.Background); break; case "HotFocus": - HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal.Background); - if (Focus.Foreground != attribute.Background) - Focus = Application.Driver.MakeAttribute (Focus.Foreground, attribute.Background); + HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal?.Background ?? default); + if (Focus?.Foreground != attribute.Background) + Focus = Application.Driver.MakeAttribute (Focus?.Foreground ?? default, attribute.Background); break; } break; @@ -285,31 +408,31 @@ Attribute SetAttribute (Attribute attribute, [CallerMemberName] string callerMem case "Menu": switch (callerMemberName) { case "Normal": - if (Focus.Background != attribute.Background) - Focus = Application.Driver.MakeAttribute (attribute.Foreground, Focus.Background); - HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background); - Disabled = Application.Driver.MakeAttribute (Disabled.Foreground, attribute.Background); + if (Focus?.Background != attribute.Background) + Focus = Application.Driver.MakeAttribute (attribute.Foreground, Focus?.Background ?? default); + HotNormal = Application.Driver.MakeAttribute (HotNormal?.Foreground ?? default, attribute.Background); + Disabled = Application.Driver.MakeAttribute (Disabled?.Foreground ?? default, attribute.Background); break; case "Focus": - Normal = Application.Driver.MakeAttribute (attribute.Foreground, Normal.Background); - HotFocus = Application.Driver.MakeAttribute (HotFocus.Foreground, attribute.Background); + Normal = Application.Driver.MakeAttribute (attribute.Foreground, Normal?.Background ?? default); + HotFocus = Application.Driver.MakeAttribute (HotFocus?.Foreground ?? default, attribute.Background); break; case "HotNormal": - if (Focus.Background != attribute.Background) - HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus.Background); - Normal = Application.Driver.MakeAttribute (Normal.Foreground, attribute.Background); - Disabled = Application.Driver.MakeAttribute (Disabled.Foreground, attribute.Background); + if (Focus?.Background != attribute.Background) + HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus?.Background ?? default); + Normal = Application.Driver.MakeAttribute (Normal?.Foreground ?? default, attribute.Background); + Disabled = Application.Driver.MakeAttribute (Disabled?.Foreground ?? default, attribute.Background); break; case "HotFocus": - HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal.Background); - if (Focus.Foreground != attribute.Background) - Focus = Application.Driver.MakeAttribute (Focus.Foreground, attribute.Background); + HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal?.Background ?? default); + if (Focus?.Foreground != attribute.Background) + Focus = Application.Driver.MakeAttribute (Focus?.Foreground ?? default, attribute.Background); break; case "Disabled": - if (Focus.Background != attribute.Background) - HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus.Background); - Normal = Application.Driver.MakeAttribute (Normal.Foreground, attribute.Background); - HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background); + if (Focus?.Background != attribute.Background) + HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus?.Background ?? default); + Normal = Application.Driver.MakeAttribute (Normal?.Foreground ?? default, attribute.Background); + HotNormal = Application.Driver.MakeAttribute (HotNormal?.Foreground ?? default, attribute.Background); break; } break; @@ -317,24 +440,24 @@ Attribute SetAttribute (Attribute attribute, [CallerMemberName] string callerMem case "Dialog": switch (callerMemberName) { case "Normal": - if (Focus.Background != attribute.Background) - Focus = Application.Driver.MakeAttribute (attribute.Foreground, Focus.Background); - HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background); + if (Focus?.Background != attribute.Background) + Focus = Application.Driver.MakeAttribute (attribute.Foreground, Focus?.Background ?? default); + HotNormal = Application.Driver.MakeAttribute (HotNormal?.Foreground ?? default, attribute.Background); break; case "Focus": - Normal = Application.Driver.MakeAttribute (attribute.Foreground, Normal.Background); - HotFocus = Application.Driver.MakeAttribute (HotFocus.Foreground, attribute.Background); + Normal = Application.Driver.MakeAttribute (attribute.Foreground, Normal?.Background ?? default); + HotFocus = Application.Driver.MakeAttribute (HotFocus?.Foreground ?? default, attribute.Background); break; case "HotNormal": - if (Focus.Background != attribute.Background) - HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus.Background); - if (Normal.Foreground != attribute.Background) - Normal = Application.Driver.MakeAttribute (Normal.Foreground, attribute.Background); + if (Focus?.Background != attribute.Background) + HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, HotFocus?.Background ?? default); + if (Normal?.Foreground != attribute.Background) + Normal = Application.Driver.MakeAttribute (Normal?.Foreground ?? default, attribute.Background); break; case "HotFocus": - HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal.Background); - if (Focus.Foreground != attribute.Background) - Focus = Application.Driver.MakeAttribute (Focus.Foreground, attribute.Background); + HotNormal = Application.Driver.MakeAttribute (attribute.Foreground, HotNormal?.Background ?? default); + if (Focus?.Foreground != attribute.Background) + Focus = Application.Driver.MakeAttribute (Focus?.Foreground ?? default, attribute.Background); break; } break; @@ -342,13 +465,13 @@ Attribute SetAttribute (Attribute attribute, [CallerMemberName] string callerMem case "Error": switch (callerMemberName) { case "Normal": - HotNormal = Application.Driver.MakeAttribute (HotNormal.Foreground, attribute.Background); - HotFocus = Application.Driver.MakeAttribute (HotFocus.Foreground, attribute.Background); + HotNormal = Application.Driver.MakeAttribute (HotNormal?.Foreground ?? default, attribute.Background); + HotFocus = Application.Driver.MakeAttribute (HotFocus?.Foreground ?? default, attribute.Background); break; case "HotNormal": case "HotFocus": HotFocus = Application.Driver.MakeAttribute (attribute.Foreground, attribute.Background); - Normal = Application.Driver.MakeAttribute (Normal.Foreground, attribute.Background); + Normal = Application.Driver.MakeAttribute (Normal?.Foreground ?? default, attribute.Background); break; } break; @@ -670,6 +793,23 @@ public abstract class ConsoleDriver { /// public virtual int [,,] Contents { get; } + + /// + /// Determinates if the current console driver supports TrueColor output- + /// + public virtual bool SupportsTrueColorOutput { get => false; } + + bool useTrueColor; + + /// + /// Controls the TureColor output mode. Can be only enabled if the underlying ConsoleDriver supports it. + /// Note this will be enabled automaticaly if supported. See also + /// + public bool UseTrueColor { + get => useTrueColor; + set => this.useTrueColor = value && SupportsTrueColorOutput; + } + /// /// Initializes the driver /// @@ -681,7 +821,7 @@ public abstract class ConsoleDriver { /// Column to move the cursor to. /// Row to move the cursor to. public abstract void Move (int col, int row); - + /// /// Adds the specified rune to the display at the current cursor position. /// diff --git a/Terminal.Gui/Core/Graphs/Annotations.cs b/Terminal.Gui/Core/Graphs/Annotations.cs index 226aab8392..0ee52f11ea 100644 --- a/Terminal.Gui/Core/Graphs/Annotations.cs +++ b/Terminal.Gui/Core/Graphs/Annotations.cs @@ -172,7 +172,7 @@ public void Render (GraphView graph) foreach (var entry in entries) { - if (entry.Item1.Color.HasValue) { + if (entry.Item1.Color != null) { Application.Driver.SetAttribute (entry.Item1.Color.Value); } else { graph.SetDriverColorToGraphColor (); @@ -226,7 +226,7 @@ public class PathAnnotation : IAnnotation { /// /// Color for the line that connects points /// - public Attribute? LineColor { get; set; } + public Attribute LineColor { get; set; } /// /// The symbol that gets drawn along the line, defaults to '.' diff --git a/Terminal.Gui/Core/Graphs/GraphCellToRender.cs b/Terminal.Gui/Core/Graphs/GraphCellToRender.cs index 402fafef33..0a904f67f9 100644 --- a/Terminal.Gui/Core/Graphs/GraphCellToRender.cs +++ b/Terminal.Gui/Core/Graphs/GraphCellToRender.cs @@ -15,7 +15,7 @@ public class GraphCellToRender { /// /// Optional color to render the with /// - public Attribute? Color { get; set; } + public Attribute Color { get; set; } /// /// Creates instance and sets with default graph coloring @@ -34,12 +34,5 @@ public GraphCellToRender (Rune rune, Attribute color) : this (rune) { Color = color; } - /// - /// Creates instance and sets and (or default if null) - /// - public GraphCellToRender (Rune rune, Attribute? color) : this (rune) - { - Color = color; - } } } \ No newline at end of file diff --git a/Terminal.Gui/Core/Graphs/Series.cs b/Terminal.Gui/Core/Graphs/Series.cs index 48b66e37dc..c867c42161 100644 --- a/Terminal.Gui/Core/Graphs/Series.cs +++ b/Terminal.Gui/Core/Graphs/Series.cs @@ -42,7 +42,7 @@ public class ScatterSeries : ISeries { /// public void DrawSeries (GraphView graph, Rect drawBounds, RectangleF graphBounds) { - if (Fill.Color.HasValue) { + if (Fill.Color != null) { Application.Driver.SetAttribute (Fill.Color.Value); } @@ -173,7 +173,7 @@ public class BarSeries : ISeries { /// /// Overrides the with a fixed color /// - public Attribute? OverrideBarColor { get; set; } + public Attribute OverrideBarColor { get; set; } /// /// True to draw along the axis under the bar. Defaults @@ -188,7 +188,7 @@ public class BarSeries : ISeries { /// protected virtual GraphCellToRender AdjustColor (GraphCellToRender graphCellToRender) { - if (OverrideBarColor.HasValue) { + if (OverrideBarColor != null) { graphCellToRender.Color = OverrideBarColor; } @@ -274,7 +274,7 @@ protected virtual void DrawBarLine (GraphView graph, Point start, Point end, Bar { var adjusted = AdjustColor (beingDrawn.Fill); - if (adjusted.Color.HasValue) { + if (adjusted.Color != null) { Application.Driver.SetAttribute (adjusted.Color.Value); } diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index 4cbb36d5c2..bdedb74485 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -54,7 +54,7 @@ - net472;netstandard2.0;net6.0 + netstandard2.0;net6.0 Terminal.Gui Terminal.Gui bin\Release\Terminal.Gui.xml diff --git a/Terminal.Gui/Views/GraphView.cs b/Terminal.Gui/Views/GraphView.cs index 48c62a760e..42822df6c4 100644 --- a/Terminal.Gui/Views/GraphView.cs +++ b/Terminal.Gui/Views/GraphView.cs @@ -63,7 +63,7 @@ public class GraphView : View { /// /// The color of the background of the graph and axis/labels /// - public Attribute? GraphColor { get; set; } + public Attribute GraphColor { get; set; } /// /// Creates a new graph with a 1 to 1 graph space with absolute layout diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 6997ce5ce9..51febc00a4 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -970,7 +970,7 @@ public class ListViewRowEventArgs : EventArgs { /// The used by current row or /// null to maintain the current attribute. /// - public Attribute? RowAttribute { get; set; } + public Attribute RowAttribute { get; set; } /// /// Initializes with the current row. diff --git a/UICatalog/Scenarios/TrueColors.cs b/UICatalog/Scenarios/TrueColors.cs new file mode 100644 index 0000000000..fc2fce3cb4 --- /dev/null +++ b/UICatalog/Scenarios/TrueColors.cs @@ -0,0 +1,116 @@ +using System; +using Terminal.Gui; + +namespace UICatalog.Scenarios { + + [ScenarioMetadata (Name: "True Colors", Description: "Demonstration of true color support.")] + [ScenarioCategory ("Colors")] + public class TrueColors : Scenario { + + public override void Setup () + { + var x = 2; + var y = 1; + + var canTrueColor = Application.Driver.SupportsTrueColorOutput; + + var lblDriverName = new Label($"Current driver is {Application.Driver.GetType().Name}") { + X = x, + Y = y++ + }; + Win.Add(lblDriverName); + y++; + + var cbSupportsTrueColor = new CheckBox ("Driver supports true color ") { + X = x, + Y = y++, + Checked = canTrueColor, + CanFocus = false + }; + Win.Add (cbSupportsTrueColor); + + var cbUseTrueColor = new CheckBox ("Use true color") { + X = x, + Y = y++, + Checked = Application.Driver.UseTrueColor, + Enabled = canTrueColor, + }; + cbUseTrueColor.Toggled += (_) => Application.Driver.UseTrueColor = cbUseTrueColor.Checked; + Win.Add (cbUseTrueColor); + + y += 2; + SetupGradient ("Red gradient", x, ref y, (i) => new TrueColor (i, 0, 0)); + SetupGradient ("Green gradient", x, ref y, (i) => new TrueColor (0, i, 0)); + SetupGradient ("Blue gradient", x, ref y, (i) => new TrueColor (0, 0, i)); + SetupGradient ("Yellow gradient", x, ref y, (i) => new TrueColor (i, i, 0)); + SetupGradient ("Magenta gradient", x, ref y, (i) => new TrueColor (i, 0, i)); + SetupGradient ("Cyan gradient", x, ref y, (i) => new TrueColor (0, i, i)); + SetupGradient ("Gray gradient", x, ref y, (i) => new TrueColor (i, i, i)); + + Win.Add (new Label ("Mouse over to get the gradient view color:") { + X = Pos.AnchorEnd (44), + Y = 2 + }); + Win.Add (new Label ("Red:") { + X = Pos.AnchorEnd (44), + Y = 4 + }); + Win.Add (new Label ("Green:") { + X = Pos.AnchorEnd (44), + Y = 5 + }); + Win.Add (new Label ("Blue:") { + X = Pos.AnchorEnd (44), + Y = 6 + }); + + var lblRed = new Label ("na") { + X = Pos.AnchorEnd (32), + Y = 4 + }; + Win.Add (lblRed); + var lblGreen = new Label ("na") { + X = Pos.AnchorEnd (32), + Y = 5 + }; + Win.Add (lblGreen); + var lblBlue = new Label ("na") { + X = Pos.AnchorEnd (32), + Y = 6 + }; + Win.Add (lblBlue); + + Application.RootMouseEvent = (e) => { + if (e.View != null) { + if (e.View.GetNormalColor () is TrueColorAttribute colorAttribute) { + lblRed.Text = colorAttribute.TrueColorForeground.Red.ToString(); + lblGreen.Text = colorAttribute.TrueColorForeground.Green.ToString (); + lblBlue.Text = colorAttribute.TrueColorForeground.Blue.ToString (); + } else { + lblRed.Text = "na"; + lblGreen.Text = "na"; + lblBlue.Text = "na"; + } + } + }; + } + + private void SetupGradient (string name, int x, ref int y, Func colorFunc) + { + var gradient = new Label (name) { + X = x, + Y = y++, + }; + Win.Add (gradient); + for (int dx = x, i = 0; i <= 256; i += 4) { + var l = new Label (" ") { + X = dx++, + Y = y, + ColorScheme = new ColorScheme () { Normal = new TrueColorAttribute (colorFunc(i > 255 ? 255 : i)) } + }; + Win.Add (l); + } + y+=2; + } + } +} \ No newline at end of file diff --git a/UnitTests/TureColorAttributeTests.cs b/UnitTests/TureColorAttributeTests.cs new file mode 100644 index 0000000000..0ac83b2395 --- /dev/null +++ b/UnitTests/TureColorAttributeTests.cs @@ -0,0 +1,84 @@ +using Xunit; + +// Alias Console to MockConsole so we don't accidentally use Console + +namespace Terminal.Gui.ConsoleDrivers { + + public class TrueColorAttributeTests { + + [Fact] + public void Constuctors_Constuct () + { + var driver = new FakeDriver (); + Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true))); + driver.Init (() => { }); + + // Test foreground, background + var fg = new TrueColor (255, 0, 0); + var bg = new TrueColor (0, 255, 0); + + var attr = new TrueColorAttribute (fg, bg); + + Assert.Equal (fg, attr.TrueColorForeground); + Assert.Equal (bg, attr.TrueColorBackground); + + // Test unified color + attr = new TrueColorAttribute (fg); + Assert.Equal (fg, attr.TrueColorForeground); + Assert.Equal (fg, attr.TrueColorBackground); + + attr = new TrueColorAttribute (bg); + Assert.Equal (bg, attr.TrueColorForeground); + Assert.Equal (bg, attr.TrueColorBackground); + + driver.End (); + Application.Shutdown (); + } + + [Fact] + public void Basic_Colors_Fallback () + { + var driver = new FakeDriver (); + Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true))); + driver.Init (() => { }); + + // Test bright basic colors + var attr = new TrueColorAttribute (new TrueColor (128, 0, 0), new TrueColor (0, 128, 0)); + Assert.Equal (Color.Red, attr.Foreground); + Assert.Equal (Color.Green, attr.Background); + + attr = new TrueColorAttribute (new TrueColor (128, 128, 0), new TrueColor (0, 0, 128)); + Assert.Equal (Color.Brown, attr.Foreground); + Assert.Equal (Color.Blue, attr.Background); + + attr = new TrueColorAttribute (new TrueColor (128, 0, 128), new TrueColor (0, 128, 128)); + Assert.Equal (Color.Magenta, attr.Foreground); + Assert.Equal (Color.Cyan, attr.Background); + + // Test basic colors + attr = new TrueColorAttribute (new TrueColor (255, 0, 0), new TrueColor (0, 255, 0)); + Assert.Equal (Color.BrightRed, attr.Foreground); + Assert.Equal (Color.BrightGreen, attr.Background); + + attr = new TrueColorAttribute (new TrueColor (255, 255, 0), new TrueColor (0, 0, 255)); + Assert.Equal (Color.BrightYellow, attr.Foreground); + Assert.Equal (Color.BrightBlue, attr.Background); + + attr = new TrueColorAttribute (new TrueColor (255, 0, 255), new TrueColor (0, 255, 255)); + Assert.Equal (Color.BrightMagenta, attr.Foreground); + Assert.Equal (Color.BrightCyan, attr.Background); + + // Test gray basic colors + attr = new TrueColorAttribute (new TrueColor (128, 128, 128), new TrueColor (255, 255, 255)); + Assert.Equal (Color.DarkGray, attr.Foreground); + Assert.Equal (Color.White, attr.Background); + + attr = new TrueColorAttribute (new TrueColor (192, 192, 192), new TrueColor (0, 0, 0)); + Assert.Equal (Color.Gray, attr.Foreground); + Assert.Equal (Color.Black, attr.Background); + + driver.End (); + Application.Shutdown (); + } + } +} \ No newline at end of file