Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
63 changes: 61 additions & 2 deletions TUI.Test/SpectreConsoleProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
Expand Down Expand Up @@ -224,4 +225,62 @@ public void SetCursorVisibilityWritesToTheInjectedConsoleWithoutMovingTheCursor(

Assert.AreEqual(expected, output.ToString());
}

/// <summary>
/// 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).
/// </summary>
[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());
}

/// <summary>
/// Tests that a zero-alpha background color writes no background SGR parameter, whatever its
/// RGB channels (ktsu-dev/TUI#155).
/// </summary>
[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());
}

/// <summary>
/// Tests that a transparent foreground writes no foreground SGR parameter, so the text keeps the
/// terminal's own color (ktsu-dev/TUI#155).
/// </summary>
[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());
}

/// <summary>
/// Tests that a partially transparent color is drawn as if opaque, since a terminal cannot blend
/// (ktsu-dev/TUI#155).
/// </summary>
[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());
}
}
8 changes: 8 additions & 0 deletions TUI/Models/TextStyle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ public readonly record struct TextStyle
/// <summary>
/// Gets or sets the foreground color
/// </summary>
/// <remarks>
/// A color with zero alpha, such as <see cref="Color.Transparent"/>, leaves the terminal's own
/// foreground. Terminals cannot blend, so any other alpha is drawn as if opaque.
/// </remarks>
public Color? ForegroundColor { get; init; }

/// <summary>
/// Gets or sets the background color
/// </summary>
/// <remarks>
/// A color with zero alpha, such as <see cref="Color.Transparent"/>, leaves the terminal's own
/// background. Terminals cannot blend, so any other alpha is drawn as if opaque.
/// </remarks>
public Color? BackgroundColor { get; init; }

/// <summary>
Expand Down
13 changes: 7 additions & 6 deletions TUI/Services/SpectreConsoleProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,16 +173,17 @@ private static string BuildStyleString(TextStyle style)
{
List<string> 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)
Expand Down
Loading