diff --git a/.gitignore b/.gitignore index dc0470a..e043c9f 100644 --- a/.gitignore +++ b/.gitignore @@ -203,6 +203,11 @@ PublishScripts/ **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ +# and except a Unity project's Packages/, which is source: Unity's package manifest and its +# resolved lock file are both meant to be committed, and a NuGet restore folder never contains +# a file by either name. +!**/[Pp]ackages/manifest.json +!**/[Pp]ackages/packages-lock.json # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files @@ -651,3 +656,16 @@ Temporary Items # ImGui.ini files imgui.ini + +# Game engine projects +# +# Godot: the import cache, and the mono/temp bin+obj a C# build writes. +.godot/ + +# Unity: .meta files are source, not the Visual Studio C++ build artifact that the `*.meta` rule +# further up targets. Unity generates one per asset and it carries the GUID that scenes, prefabs +# and serialized references point at, so ignoring them gives every clone fresh GUIDs and silently +# breaks those references - including for a plug-in whose .dll is itself a build output. This +# negation has to come after that rule to win, and is scoped to the asset tree so the Visual +# Studio artifact stays ignored everywhere else. +!**/[Aa]ssets/**/*.meta diff --git a/TUI.Test/SpectreConsoleProviderTests.cs b/TUI.Test/SpectreConsoleProviderTests.cs index 232feff..d2666ad 100644 --- a/TUI.Test/SpectreConsoleProviderTests.cs +++ b/TUI.Test/SpectreConsoleProviderTests.cs @@ -17,13 +17,14 @@ public sealed class SpectreConsoleProviderTests private const int Width = 10; private const int Height = 5; - private static (SpectreConsoleProvider Provider, StringWriter Output) CreateProvider() + private static (SpectreConsoleProvider Provider, StringWriter Output) CreateProvider( + ColorSystemSupport colorSystem = ColorSystemSupport.NoColors) { StringWriter output = new(); IAnsiConsole console = AnsiConsole.Create(new AnsiConsoleSettings { Ansi = AnsiSupport.Yes, - ColorSystem = ColorSystemSupport.NoColors, + ColorSystem = colorSystem, Interactive = InteractionSupport.No, Out = new AnsiConsoleOutput(output), }); @@ -224,4 +225,62 @@ public void SetCursorVisibilityWritesToTheInjectedConsoleWithoutMovingTheCursor( Assert.AreEqual(expected, output.ToString()); } + + /// + /// Tests that a fully transparent background is left to the terminal rather than painted as the + /// opaque color its RGB channels happen to hold (ktsu-dev/TUI#155). + /// + [TestMethod] + public void TransparentBackgroundNameWritesNoBackground() + { + (SpectreConsoleProvider provider, StringWriter output) = CreateProvider(ColorSystemSupport.TrueColor); + + provider.WriteAt("Hi", new Position(0, 0), new TextStyle { Foreground = "Yellow", Background = "Transparent" }); + + Assert.AreEqual("\u001b[1;1H\u001b[38;2;255;255;0mHi\u001b[0m", output.ToString()); + } + + /// + /// Tests that a zero-alpha background color writes no background SGR parameter, whatever its + /// RGB channels (ktsu-dev/TUI#155). + /// + [TestMethod] + public void ZeroAlphaBackgroundColorWritesNoBackground() + { + (SpectreConsoleProvider provider, StringWriter output) = CreateProvider(ColorSystemSupport.TrueColor); + + provider.WriteAt("Hi", new Position(0, 0), new TextStyle { BackgroundColor = System.Drawing.Color.FromArgb(0, 0, 0, 0), IsBold = true }); + + Assert.DoesNotContain("48;", output.ToString()); + Assert.Contains("Hi", output.ToString()); + } + + /// + /// Tests that a transparent foreground writes no foreground SGR parameter, so the text keeps the + /// terminal's own color (ktsu-dev/TUI#155). + /// + [TestMethod] + public void TransparentForegroundWritesNoForeground() + { + (SpectreConsoleProvider provider, StringWriter output) = CreateProvider(ColorSystemSupport.TrueColor); + + provider.WriteAt("Hi", new Position(0, 0), new TextStyle { Foreground = "Transparent", Background = "Blue" }); + + Assert.DoesNotContain("38;", output.ToString()); + Assert.Contains("48;2;0;0;255", output.ToString()); + } + + /// + /// Tests that a partially transparent color is drawn as if opaque, since a terminal cannot blend + /// (ktsu-dev/TUI#155). + /// + [TestMethod] + public void PartiallyTransparentBackgroundIsDrawnOpaque() + { + (SpectreConsoleProvider provider, StringWriter output) = CreateProvider(ColorSystemSupport.TrueColor); + + provider.WriteAt("Hi", new Position(0, 0), new TextStyle { BackgroundColor = System.Drawing.Color.FromArgb(128, 0, 0, 255) }); + + Assert.Contains("48;2;0;0;255", output.ToString()); + } } diff --git a/TUI/Models/TextStyle.cs b/TUI/Models/TextStyle.cs index 107aaba..36a756f 100644 --- a/TUI/Models/TextStyle.cs +++ b/TUI/Models/TextStyle.cs @@ -13,11 +13,19 @@ public readonly record struct TextStyle /// /// Gets or sets the foreground color /// + /// + /// A color with zero alpha, such as , leaves the terminal's own + /// foreground. Terminals cannot blend, so any other alpha is drawn as if opaque. + /// public Color? ForegroundColor { get; init; } /// /// Gets or sets the background color /// + /// + /// A color with zero alpha, such as , leaves the terminal's own + /// background. Terminals cannot blend, so any other alpha is drawn as if opaque. + /// public Color? BackgroundColor { get; init; } /// diff --git a/TUI/Services/SpectreConsoleProvider.cs b/TUI/Services/SpectreConsoleProvider.cs index 90ea4de..c47c5dd 100644 --- a/TUI/Services/SpectreConsoleProvider.cs +++ b/TUI/Services/SpectreConsoleProvider.cs @@ -173,16 +173,17 @@ private static string BuildStyleString(TextStyle style) { List parts = []; - if (style.ForegroundColor.HasValue) + // A terminal cannot blend, so a fully transparent color means "leave the terminal's own" and + // any other alpha is drawn opaque. Writing the RGB of Color.Transparent would paint white + // (ktsu-dev/TUI#155). + if (style.ForegroundColor is { A: > 0 } foreground) { - System.Drawing.Color color = style.ForegroundColor.Value; - parts.Add($"#{color.R:X2}{color.G:X2}{color.B:X2}"); + parts.Add($"#{foreground.R:X2}{foreground.G:X2}{foreground.B:X2}"); } - if (style.BackgroundColor.HasValue) + if (style.BackgroundColor is { A: > 0 } background) { - System.Drawing.Color color = style.BackgroundColor.Value; - parts.Add($"on #{color.R:X2}{color.G:X2}{color.B:X2}"); + parts.Add($"on #{background.R:X2}{background.G:X2}{background.B:X2}"); } if (style.IsBold)