diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index c2ba8e1115..c5a18c3bff 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -1526,19 +1526,19 @@ System.Text.StringBuilder WriteAttributes (Attribute attr) { System.Text.StringBuilder sb = new System.Text.StringBuilder (); - if ((UseTrueColor) && (attr is TrueColorAttribute tca)) { + if (UseTrueColor) { sb.Append (new [] { '\x1b', '[', '3', '8', ';', '2', ';' }); - sb.Append (tca.TrueColorForeground.Red); + sb.Append (attr.TrueColorForeground.Red); sb.Append (';'); - sb.Append (tca.TrueColorForeground.Green); + sb.Append (attr.TrueColorForeground.Green); sb.Append (';'); - sb.Append (tca.TrueColorForeground.Blue); + sb.Append (attr.TrueColorForeground.Blue); sb.Append (new [] { ';', '4', '8', ';', '2', ';' }); - sb.Append (tca.TrueColorBackground.Red); + sb.Append (attr.TrueColorBackground.Red); sb.Append (';'); - sb.Append (tca.TrueColorBackground.Green); + sb.Append (attr.TrueColorBackground.Green); sb.Append (';'); - sb.Append (tca.TrueColorBackground.Blue); + sb.Append (attr.TrueColorBackground.Blue); sb.Append ('m'); } else { const string CSI = "\x1b["; diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index a52e08f7d1..2498db682c 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -111,7 +111,9 @@ private bool WriteConsoleTrueColorOutput(ExtendedCharInfo [] charInfoBuffer) if (attr != prev) { prev = attr; - if (attr is TrueColorAttribute tca) { + + // TODO: this is now always true + if (attr is Attribute tca) { stringBuilder.Append (SendTrueColorFg); stringBuilder.Append (tca.TrueColorForeground.Red); stringBuilder.Append (';'); diff --git a/Terminal.Gui/Core/ConsoleDriver.cs b/Terminal.Gui/Core/ConsoleDriver.cs index 6deaea8a8f..ac36c04cf0 100644 --- a/Terminal.Gui/Core/ConsoleDriver.cs +++ b/Terminal.Gui/Core/ConsoleDriver.cs @@ -173,15 +173,24 @@ public class Attribute : IEquatable { /// /// The color attribute value. /// - public int Value { get; } + public int Value { get; private set; } /// /// The foreground color. /// - public Color Foreground { get; } + public Color Foreground { get; private set; } /// /// The background color. /// - public Color Background { get; } + public Color Background { get; private set; } + + /// + /// The foreground color as a . + /// + public TrueColor TrueColorForeground { get; private set; } + /// + /// The background color as a . + /// + public TrueColor TrueColorBackground { get; private set; } /// /// Initializes a new instance of the class with only the value passed to @@ -199,10 +208,12 @@ public Attribute (int value = 0) Value = value; Foreground = foreground; Background = background; + + PopulateTrueColorsFromConsoleColors (); } /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// /// Value. /// Foreground @@ -212,10 +223,12 @@ public Attribute (int value, Color foreground, Color background) Value = value; Foreground = foreground; Background = background; + + PopulateTrueColorsFromConsoleColors (); } /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// /// Foreground /// Background @@ -224,15 +237,55 @@ public Attribute (Color foreground, Color background) Value = Make (foreground, background).Value; Foreground = foreground; Background = background; + + PopulateTrueColorsFromConsoleColors (); } /// - /// Initializes a new instance of the struct + /// Initializes a new instance of the class /// with the same colors for the foreground and background. /// /// The color. public Attribute (Color color) : this (color, color) { } + /// + /// Initializes a new instance of the class. Populates + /// and . Also computes + /// and (basic console colors) in case + /// driver does not support true color rendering. + /// + /// + /// + public Attribute (TrueColor trueColorForeground, TrueColor trueColorBackground) + { + TrueColorForeground = trueColorForeground; + TrueColorBackground = trueColorBackground; + + PopulateConsoleColorsFromTrueColors (); + } + + + /// + /// + /// Initializes a new instance of the class. Populates + /// and with explicit + /// fallback values for and (in case + /// driver does not support true color rendering). + /// + /// If you do not want to manually specify the fallback colors use + /// instead which auto calculates these. + /// + /// True color RGB values you would like to use. + /// True color RGB values you would like to use. + /// Simple console color replacement if driver does not support true color. + /// Simple console color replacement if driver does not support true color. + public Attribute (TrueColor trueColorForeground, TrueColor trueColorBackground, Color foreground, Color background) + : this (foreground, background) + { + TrueColorForeground = trueColorForeground; + TrueColorBackground = trueColorBackground; + } + /// public bool Equals (Attribute other) { @@ -281,39 +334,94 @@ public static Attribute Get () /// 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. + /// Populates and based on + /// and console colors. /// - /// Foreground - /// Background - public TrueColorAttribute (TrueColor foreground, TrueColor background) - : base ((Color)TrueColor.GetCode4 (foreground), (Color)TrueColor.GetCode4 (background)) + private void PopulateTrueColorsFromConsoleColors () { - TrueColorForeground = foreground; - TrueColorBackground = background; + TrueColorForeground = ConsoleColorToTrueColor (Foreground); + TrueColorBackground = ConsoleColorToTrueColor (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) { } + private TrueColor ConsoleColorToTrueColor (Color consoleColor) + { + // Brown in cmd is + // C1 9C 00 + + switch (consoleColor) { + case Color.Black: return new TrueColor (0, 0, 0); + case Color.Blue: return new TrueColor (0, 0, 0x80); + case Color.Green: return new TrueColor (0, 0x80, 0); + case Color.Cyan: return new TrueColor (0, 0x80, 0x80); + case Color.Red: return new TrueColor (0x80, 0, 0); + case Color.Magenta: return new TrueColor (0x80, 0, 0x80); + case Color.Brown: return new TrueColor (0xC1, 0x9C, 0x00); // TODO confirm this + case Color.Gray: return new TrueColor (0xC0, 0xC0, 0xC0); + case Color.DarkGray: return new TrueColor (0x80, 0x80, 0x80); + case Color.BrightBlue: return new TrueColor (0, 0, 0xFF); + case Color.BrightGreen: return new TrueColor (0, 0xFF, 0); + case Color.BrightCyan: return new TrueColor (0, 0xFF, 0xFF); + case Color.BrightRed: return new TrueColor (0xFF, 0, 0); + case Color.BrightMagenta: return new TrueColor (0xFF, 0, 0xFF); + case Color.BrightYellow: return new TrueColor (0xFF, 0xFF, 0); + case Color.White: return new TrueColor (0xFF, 0xFF, 0xFF); + default: throw new ArgumentOutOfRangeException (nameof (consoleColor)); + }; + } - /// - /// The foreground color. - /// - public TrueColor TrueColorForeground { get; } - /// - /// The background color. - /// - public TrueColor TrueColorBackground { get; } + private void PopulateConsoleColorsFromTrueColors () + { + Foreground = TrueColorToConsoleColor (TrueColorForeground); + Background = TrueColorToConsoleColor (TrueColorBackground); + Value = Make (Foreground, Background).Value; + } + + private Color TrueColorToConsoleColor (TrueColor trueColor) + { + // The points we could return from + var lookup = new Dictionary () { + { new TrueColor(0,0,0),Color.Black}, + { new TrueColor (0, 0, 0x80),Color.Blue}, + { new TrueColor (0, 0x80, 0),Color.Green}, + { new TrueColor (0, 0x80, 0x80),Color.Cyan}, + { new TrueColor (0x80, 0, 0),Color.Red}, + { new TrueColor (0x80, 0, 0x80),Color.Magenta}, + { new TrueColor (0xC1, 0x9C, 0x00),Color.Brown}, // TODO confirm this + { new TrueColor (0xC0, 0xC0, 0xC0),Color.Gray}, + { new TrueColor (0x80, 0x80, 0x80),Color.DarkGray}, + { new TrueColor (0, 0, 0xFF),Color.BrightBlue}, + { new TrueColor (0, 0xFF, 0),Color.BrightGreen}, + { new TrueColor (0, 0xFF, 0xFF),Color.BrightCyan}, + { new TrueColor (0xFF, 0, 0),Color.BrightRed}, + { new TrueColor (0xFF, 0, 0xFF),Color.BrightMagenta }, + { new TrueColor (0xFF, 0xFF, 0),Color.BrightYellow}, + { new TrueColor (0xFF, 0xFF, 0xFF),Color.White}, + }; + + var distances = lookup.Select ( + k => Tuple.Create ( + // the candidate we are considering matching against (RGB) + k.Key, + + CalculateDistance (k.Key, trueColor) + )); + + // get the closest + var match = distances.OrderBy (t => t.Item2).First (); + + return lookup [match.Item1]; + } + + private float CalculateDistance (TrueColor color1, TrueColor color2) + { + // use RGB distance + return + Math.Abs (color1.Red - color2.Red) + + Math.Abs (color1.Green - color2.Green) + + Math.Abs (color1.Blue - color2.Blue); + } } /// diff --git a/UICatalog/Scenarios/Images.cs b/UICatalog/Scenarios/Images.cs new file mode 100644 index 0000000000..7496ffec7e --- /dev/null +++ b/UICatalog/Scenarios/Images.cs @@ -0,0 +1,133 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.ColorSpaces; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using Terminal.Gui; +using Attribute = Terminal.Gui.Attribute; + +namespace UICatalog.Scenarios { + [ScenarioMetadata (Name: "Images", Description: "Demonstration of how to render an image with/without true color support.")] + [ScenarioCategory ("Colors")] + public class Images : Scenario + { + public override void Setup () + { + base.Setup (); + + var x = 0; + var y = 0; + + 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); + + var btnOpenImage = new Button ("Open Image") { + X = x, + Y = y++ + }; + Win.Add (btnOpenImage); + + var imageView = new ImageView () { + X = x, + Y = y++, + Width = Dim.Fill(), + Height = Dim.Fill(), + }; + Win.Add (imageView); + + btnOpenImage.Clicked += () => { + var ofd = new OpenDialog ("Open Image", "Image"); + Application.Run (ofd); + + var path = ofd.FilePath.ToString (); + + if (string.IsNullOrWhiteSpace (path)) { + return; + } + + if(!File.Exists(path)) { + return; + } + + Image img; + + try { + img = Image.Load (File.ReadAllBytes (path)); + } catch (Exception ex) { + + MessageBox.ErrorQuery ("Could not open file", ex.Message, "Ok"); + return; + } + + imageView.SetImage(img); + }; + } + + class ImageView : View { + + private Image fullResImage; + private Image matchSize; + + ConcurrentDictionary cache = new ConcurrentDictionary(); + + internal void SetImage (Image image) + { + fullResImage = image; + this.SetNeedsDisplay (); + } + + public override void Redraw (Rect bounds) + { + base.Redraw (bounds); + + if (fullResImage == null) { + return; + } + + // if we have not got a cached resized image of this size + if(matchSize == null || bounds.Width != matchSize.Width || bounds.Height != matchSize.Height) { + + // generate one + matchSize = fullResImage.Clone (x => x.Resize (bounds.Width, bounds.Height)); + } + + for (int y = 0; y < bounds.Height; y++) { + for (int x = 0; x < bounds.Width; x++) { + var rgb = matchSize [x, y]; + + var attr = cache.GetOrAdd (rgb, (rgb) => new Attribute (new TrueColor (), new TrueColor (rgb.R, rgb.G, rgb.B))); + + Driver.SetAttribute(attr); + AddRune (x, y, ' '); + } + } + } + } + } +} \ No newline at end of file diff --git a/UICatalog/Scenarios/TrueColors.cs b/UICatalog/Scenarios/TrueColors.cs index fc2fce3cb4..f294b6ac82 100644 --- a/UICatalog/Scenarios/TrueColors.cs +++ b/UICatalog/Scenarios/TrueColors.cs @@ -81,16 +81,11 @@ public override void Setup () Win.Add (lblBlue); Application.RootMouseEvent = (e) => { + var normal = e.View.GetNormalColor (); 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"; - } + lblRed.Text = normal.TrueColorForeground.Red.ToString(); + lblGreen.Text = normal.TrueColorForeground.Green.ToString (); + lblBlue.Text = normal.TrueColorForeground.Blue.ToString (); } }; } @@ -106,7 +101,10 @@ private void SetupGradient (string name, int x, ref int y, Func var l = new Label (" ") { X = dx++, Y = y, - ColorScheme = new ColorScheme () { Normal = new TrueColorAttribute (colorFunc(i > 255 ? 255 : i)) } + ColorScheme = new ColorScheme () { Normal = new Terminal.Gui.Attribute ( + colorFunc (i > 255 ? 255 : i), + colorFunc (i > 255 ? 255 : i) + ) } }; Win.Add (l); } diff --git a/UICatalog/UICatalog.csproj b/UICatalog/UICatalog.csproj index 234474a265..bdbdcddf00 100644 --- a/UICatalog/UICatalog.csproj +++ b/UICatalog/UICatalog.csproj @@ -18,6 +18,9 @@ TRACE;DEBUG_IDISPOSABLE + + + diff --git a/UnitTests/TureColorAttributeTests.cs b/UnitTests/TureColorAttributeTests.cs index 0ac83b2395..9d3c9c1ee2 100644 --- a/UnitTests/TureColorAttributeTests.cs +++ b/UnitTests/TureColorAttributeTests.cs @@ -1,3 +1,4 @@ +using System; using Xunit; // Alias Console to MockConsole so we don't accidentally use Console @@ -17,17 +18,17 @@ public void Constuctors_Constuct () var fg = new TrueColor (255, 0, 0); var bg = new TrueColor (0, 255, 0); - var attr = new TrueColorAttribute (fg, bg); + var attr = new Attribute (fg, bg); Assert.Equal (fg, attr.TrueColorForeground); Assert.Equal (bg, attr.TrueColorBackground); // Test unified color - attr = new TrueColorAttribute (fg); + attr = new Attribute (fg,fg); Assert.Equal (fg, attr.TrueColorForeground); Assert.Equal (fg, attr.TrueColorBackground); - attr = new TrueColorAttribute (bg); + attr = new Attribute (bg,bg); Assert.Equal (bg, attr.TrueColorForeground); Assert.Equal (bg, attr.TrueColorBackground); @@ -35,50 +36,49 @@ public void Constuctors_Constuct () Application.Shutdown (); } - [Fact] - public void Basic_Colors_Fallback () + [InlineData (new object [] { 127, 0, 0, Color.Red })] + [InlineData(new object [] { 128, 0, 0, Color.Red})] + [InlineData (new object [] { 130, 0, 0, Color.Red })] + [InlineData (new object [] { 200, 0, 0, Color.BrightRed })] + [InlineData (new object [] { 245, 0, 0, Color.BrightRed })] + [InlineData (new object [] { 255, 0, 0, Color.BrightRed })] + + [InlineData (new object [] { 0, 128, 0, Color.Green })] + //[InlineData (new object [] { 128, 128, 0, Color.Brown })] // TODO : This was an original test in the PR I have kept it but current it does not map to Brown (it maps to BrightYellow) + [InlineData (new object [] { 0, 0, 128, Color.Blue })] + [InlineData (new object [] { 128, 0, 128, Color.Magenta })] + [InlineData (new object [] { 0, 128, 128, Color.Cyan })] + [InlineData (new object [] { 0, 255, 0, Color.BrightGreen })] + [InlineData (new object [] { 255, 255, 0, Color.BrightYellow })] + [InlineData (new object [] { 0, 0, 255, Color.BrightBlue })] + [InlineData (new object [] { 255, 0, 255, Color.BrightMagenta })] + [InlineData (new object [] { 0, 255, 255, Color.BrightCyan })] + [InlineData (new object [] { 128, 128, 128, Color.DarkGray })] + [InlineData (new object [] { 255, 255, 255, Color.White })] + [InlineData (new object [] { 192, 192, 192, Color.Gray })] + [InlineData (new object [] { 0, 0, 0, Color.Black })] + [Theory, AutoInitShutdown] + public void Basic_Colors_Fallback (int r, int g, int b, Color expectedColor) { - 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); + // Test foreground color property + var attr = new Attribute (new TrueColor (r, g, b), new TrueColor (0,0,0)); + Assert.Equal (expectedColor, attr.Foreground); + Assert.Equal (Color.Black, attr.Background); + + // Test background color property + attr = new Attribute (new TrueColor (0, 0, 0), new TrueColor (r, g, b)); + Assert.Equal (Color.Black, attr.Foreground); + Assert.Equal (expectedColor, attr.Background); + + // Test 5 up + attr = new Attribute (new TrueColor (Math.Min (255,r+5), Math.Min(255,g+5), Math.Min (255, b+5)), new TrueColor (0, 0, 0)); + Assert.Equal (expectedColor, attr.Foreground); Assert.Equal (Color.Black, attr.Background); - driver.End (); - Application.Shutdown (); + // Test 5 down + attr = new Attribute (new TrueColor (Math.Max (0, r - 5), Math.Max (0, g - 5), Math.Max (0, b - 5)), new TrueColor (0, 0, 0)); + Assert.Equal (expectedColor, attr.Foreground); + Assert.Equal (Color.Black, attr.Background); } } } \ No newline at end of file