Skip to content

Commit ddbe370

Browse files
authored
Improve the propagation of color attributes over ConPTY (#6506)
This PR reimplements the VT rendering engines to do a better job of preserving the original color types when propagating attributes over ConPTY. For the 16-color renderers it provides better support for default colors and improves the efficiency of the color narrowing conversions. It also fixes problems with the ordering of character renditions that could result in attributes being dropped. Originally the base renderer would calculate the RGB color values and legacy/extended attributes up front, passing that data on to the active engine's `UpdateDrawingBrushes` method. With this new implementation, the renderer now just passes through the original `TextAttribute` along with an `IRenderData` interface, and leaves it to the engines to extract the information they need. The GDI and DirectX engines now have to lookup the RGB colors themselves (via simple `IRenderData` calls), but have no need for the other attributes. The VT engines extract the information that they need from the `TextAttribute`, instead of having to reverse engineer it from `COLORREF`s. The process for the 256-color Xterm engine starts with a check for default colors. If both foreground and background are default, it outputs a SGR 0 reset, and clears the `_lastTextAttribute` completely to make sure any reset state is reapplied. With that out the way, the foreground and background are updated (if changed) in one of 4 ways. They can either be a default value (SGR 39 and 49), a 16-color index (using ANSI or AIX sequences), a 256-color index, or a 24-bit RGB value (both using SGR 38 and 48 sequences). Then once the colors are accounted for, there is a separate step that handles the character rendition attributes (bold, italics, underline, etc.) This step must come _after_ the color sequences, in case a SGR reset is required, which would otherwise have cleared any character rendition attributes if it came last (which is what happened in the original implementation). The process for the 16-color engines is a little different. The target client in this case (Windows telnet) is incapable of setting default colors individually, so we need to output an SGR 0 reset if _either_ color has changed to default. With that out the way, we use the `TextColor::GetLegacyIndex` method to obtain an approximate 16-color index for each color, and apply the bold attribute by brightening the foreground index (setting bit 8) if the color type permits that. However, since Windows telnet only supports the 8 basic ANSI colors, the best we can do for bright colors is to output an SGR 1 attribute to get a bright foreground. There is nothing we can do about a bright background, so after that we just have to drop the high bit from the colors. If the resulting index values have changed from what they were before, we then output ANSI 8-color SGR sequences to update them. As with the 256-color engine, there is also a final step to handle the character rendition attributes. But in this case, the only supported attributes are underline and reversed video. Since the VT engines no longer depend on the active color table and default color values, there was quite a lot of code that could now be removed. This included the `IDefaultColorProvider` interface and implementations, the `Find(Nearest)TableIndex` functions, and also the associated HLS conversion and difference calculations. VALIDATION Other than simple API parameter changes, the majority of updates required in the unit tests were to correct assumptions about the way the colors should be rendered, which were the source of the narrowing bugs this PR was trying to fix. Like passing white on black to the `UpdateDrawingBrushes` API, and expecting it to output the default `SGR 0` sequence, or passing an RGB color and expecting an indexed SGR sequence. In addition to that, I've added some VT renderer tests to make sure the rendition attributes (bold, underline, etc) are correctly retained when a default color update causes an `SGR 0` sequence to be generated (the source of bug #3076). And I've extended the VT renderer color tests (both 256-color and 16-color) to make sure we're covering all of the different color types (default, RGB, and both forms of indexed colors). I've also tried to manually verify that all of the test cases in the linked bug reports (and their associated duplicates) are now fixed when this PR is applied. Closes #2661 Closes #3076 Closes #3717 Closes #5384 Closes #5864 This is only a partial fix for #293, but I suspect the remaining cases are unfixable.
1 parent f0df154 commit ddbe370

34 files changed

+613
-795
lines changed

src/buffer/out/TextAttribute.cpp

+21-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ WORD TextAttribute::GetLegacyAttributes() const noexcept
3131
const BYTE fgIndex = _foreground.GetLegacyIndex(s_legacyDefaultForeground);
3232
const BYTE bgIndex = _background.GetLegacyIndex(s_legacyDefaultBackground);
3333
const WORD metaAttrs = _wAttrLegacy & META_ATTRS;
34-
const bool brighten = _foreground.IsIndex16() && IsBold();
34+
const bool brighten = IsBold() && _foreground.CanBeBrightened();
3535
return fgIndex | (bgIndex << 4) | metaAttrs | (brighten ? FOREGROUND_INTENSITY : 0);
3636
}
3737

@@ -90,6 +90,26 @@ COLORREF TextAttribute::_GetRgbBackground(std::basic_string_view<COLORREF> color
9090
return _background.GetColor(colorTable, defaultColor, false);
9191
}
9292

93+
TextColor TextAttribute::GetForeground() const noexcept
94+
{
95+
return _foreground;
96+
}
97+
98+
TextColor TextAttribute::GetBackground() const noexcept
99+
{
100+
return _background;
101+
}
102+
103+
void TextAttribute::SetForeground(const TextColor foreground) noexcept
104+
{
105+
_foreground = foreground;
106+
}
107+
108+
void TextAttribute::SetBackground(const TextColor background) noexcept
109+
{
110+
_background = background;
111+
}
112+
93113
void TextAttribute::SetForeground(const COLORREF rgbForeground) noexcept
94114
{
95115
_foreground = TextColor(rgbForeground);

src/buffer/out/TextAttribute.hpp

+4
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ class TextAttribute final
107107

108108
ExtendedAttributes GetExtendedAttributes() const noexcept;
109109

110+
TextColor GetForeground() const noexcept;
111+
TextColor GetBackground() const noexcept;
112+
void SetForeground(const TextColor foreground) noexcept;
113+
void SetBackground(const TextColor background) noexcept;
110114
void SetForeground(const COLORREF rgbForeground) noexcept;
111115
void SetBackground(const COLORREF rgbBackground) noexcept;
112116
void SetIndexedForeground(const BYTE fgIndex) noexcept;

src/buffer/out/TextColor.cpp

+7-2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ constexpr std::array<BYTE, 256> Index256ToIndex16 = {
5050

5151
// clang-format on
5252

53+
bool TextColor::CanBeBrightened() const noexcept
54+
{
55+
return IsIndex16() || IsDefault();
56+
}
57+
5358
bool TextColor::IsLegacy() const noexcept
5459
{
5560
return IsIndex16() || (IsIndex256() && _index < 16);
@@ -164,7 +169,7 @@ COLORREF TextColor::GetColor(std::basic_string_view<COLORREF> colorTable,
164169
}
165170
else if (IsRgb())
166171
{
167-
return _GetRGB();
172+
return GetRGB();
168173
}
169174
else if (IsIndex16() && brighten)
170175
{
@@ -214,7 +219,7 @@ BYTE TextColor::GetLegacyIndex(const BYTE defaultIndex) const noexcept
214219
// - <none>
215220
// Return Value:
216221
// - a COLORREF containing our stored value
217-
COLORREF TextColor::_GetRGB() const noexcept
222+
COLORREF TextColor::GetRGB() const noexcept
218223
{
219224
return RGB(_red, _green, _blue);
220225
}

src/buffer/out/TextColor.h

+4-3
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ struct TextColor
7777
friend constexpr bool operator==(const TextColor& a, const TextColor& b) noexcept;
7878
friend constexpr bool operator!=(const TextColor& a, const TextColor& b) noexcept;
7979

80+
bool CanBeBrightened() const noexcept;
8081
bool IsLegacy() const noexcept;
8182
bool IsIndex16() const noexcept;
8283
bool IsIndex256() const noexcept;
@@ -98,6 +99,8 @@ struct TextColor
9899
return _index;
99100
}
100101

102+
COLORREF GetRGB() const noexcept;
103+
101104
private:
102105
ColorType _meta : 2;
103106
union
@@ -107,8 +110,6 @@ struct TextColor
107110
BYTE _green;
108111
BYTE _blue;
109112

110-
COLORREF _GetRGB() const noexcept;
111-
112113
#ifdef UNIT_TESTING
113114
friend class TextBufferTests;
114115
template<typename TextColor>
@@ -149,7 +150,7 @@ namespace WEX
149150
}
150151
else if (color.IsRgb())
151152
{
152-
return WEX::Common::NoThrowString().Format(L"{RGB:0x%06x}", color._GetRGB());
153+
return WEX::Common::NoThrowString().Format(L"{RGB:0x%06x}", color.GetRGB());
153154
}
154155
else
155156
{

src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp

+1-3
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,7 @@ class TerminalCoreUnitTests::ConptyRoundtripTests final
116116
Viewport initialViewport = currentBuffer.GetViewport();
117117

118118
auto vtRenderEngine = std::make_unique<Xterm256Engine>(std::move(hFile),
119-
gci,
120-
initialViewport,
121-
gci.Get16ColorTable());
119+
initialViewport);
122120
auto pfn = std::bind(&ConptyRoundtripTests::_writeCallback, this, std::placeholders::_1, std::placeholders::_2);
123121
vtRenderEngine->SetTestCallback(pfn);
124122

src/host/VtIo.cpp

+1-7
Original file line numberDiff line numberDiff line change
@@ -155,22 +155,16 @@ VtIo::VtIo() :
155155
{
156156
case VtIoMode::XTERM_256:
157157
_pVtRenderEngine = std::make_unique<Xterm256Engine>(std::move(_hOutput),
158-
gci,
159-
initialViewport,
160-
gci.Get16ColorTable());
158+
initialViewport);
161159
break;
162160
case VtIoMode::XTERM:
163161
_pVtRenderEngine = std::make_unique<XtermEngine>(std::move(_hOutput),
164-
gci,
165162
initialViewport,
166-
gci.Get16ColorTable(),
167163
false);
168164
break;
169165
case VtIoMode::XTERM_ASCII:
170166
_pVtRenderEngine = std::make_unique<XtermEngine>(std::move(_hOutput),
171-
gci,
172167
initialViewport,
173-
gci.Get16ColorTable(),
174168
true);
175169
break;
176170
default:

src/host/conattrs.cpp

-117
Original file line numberDiff line numberDiff line change
@@ -17,100 +17,6 @@ Author(s):
1717
#include "..\inc\conattrs.hpp"
1818
#include <cmath>
1919

20-
struct _HSL
21-
{
22-
double h, s, l;
23-
24-
// constructs an HSL color from a RGB Color.
25-
explicit _HSL(const COLORREF rgb)
26-
{
27-
const double r = (double)GetRValue(rgb);
28-
const double g = (double)GetGValue(rgb);
29-
const double b = (double)GetBValue(rgb);
30-
31-
const auto [min, max] = std::minmax({ r, g, b });
32-
33-
const auto diff = max - min;
34-
const auto sum = max + min;
35-
// Luminance
36-
l = max / 255.0;
37-
38-
// Saturation
39-
s = (max == 0) ? 0 : diff / max;
40-
41-
//Hue
42-
double q = (diff == 0) ? 0 : 60.0 / diff;
43-
if (max == r)
44-
{
45-
h = (g < b) ? ((360.0 + q * (g - b)) / 360.0) : ((q * (g - b)) / 360.0);
46-
}
47-
else if (max == g)
48-
{
49-
h = (120.0 + q * (b - r)) / 360.0;
50-
}
51-
else if (max == b)
52-
{
53-
h = (240.0 + q * (r - g)) / 360.0;
54-
}
55-
else
56-
{
57-
h = 0;
58-
}
59-
}
60-
};
61-
62-
//Routine Description:
63-
// Finds the "distance" between a given HSL color and an RGB color, using the HSL color space.
64-
// This function is designed such that the caller would convert one RGB color to HSL ahead of time,
65-
// then compare many RGB colors to that first color.
66-
//Arguments:
67-
// - phslColorA - a pointer to the first color, as a HSL color.
68-
// - rgbColorB - The second color to compare, in RGB color.
69-
// Return value:
70-
// The "distance" between the two.
71-
static double _FindDifference(const _HSL* const phslColorA, const COLORREF rgbColorB)
72-
{
73-
const _HSL hslColorB = _HSL(rgbColorB);
74-
return sqrt(pow((hslColorB.h - phslColorA->h), 2) +
75-
pow((hslColorB.s - phslColorA->s), 2) +
76-
pow((hslColorB.l - phslColorA->l), 2));
77-
}
78-
79-
//Routine Description:
80-
// For a given RGB color Color, finds the nearest color from the array ColorTable, and returns the index of that match.
81-
//Arguments:
82-
// - Color - The RGB color to fine the nearest color to.
83-
// - ColorTable - The array of colors to find a nearest color from.
84-
// Return value:
85-
// The index in ColorTable of the nearest match to Color.
86-
WORD FindNearestTableIndex(const COLORREF Color, const std::basic_string_view<COLORREF> ColorTable)
87-
{
88-
// Quick check for an exact match in the color table:
89-
for (WORD i = 0; i < ColorTable.size(); i++)
90-
{
91-
if (Color == ColorTable[i])
92-
{
93-
return i;
94-
}
95-
}
96-
97-
// Did not find an exact match - do an expensive comparison to the elements
98-
// of the table to find the nearest color.
99-
const _HSL hslColor = _HSL(Color);
100-
WORD closest = 0;
101-
double minDiff = _FindDifference(&hslColor, ColorTable[0]);
102-
for (WORD i = 1; i < ColorTable.size(); i++)
103-
{
104-
double diff = _FindDifference(&hslColor, ColorTable[i]);
105-
if (diff < minDiff)
106-
{
107-
minDiff = diff;
108-
closest = i;
109-
}
110-
}
111-
return closest;
112-
}
113-
11420
// Function Description:
11521
// - Converts the value of a xterm color table index to the windows color table equivalent.
11622
// Arguments:
@@ -144,26 +50,3 @@ WORD Xterm256ToWindowsIndex(const size_t xtermTableEntry) noexcept
14450
return xtermTableEntry < 16 ? XtermToWindowsIndex(xtermTableEntry) :
14551
static_cast<WORD>(xtermTableEntry);
14652
}
147-
148-
//Routine Description:
149-
// Returns the exact entry from the color table, if it's in there.
150-
//Arguments:
151-
// - Color - The RGB color to fine the nearest color to.
152-
// - ColorTable - The array of colors to find a nearest color from.
153-
// Return value:
154-
// The index in ColorTable of the nearest match to Color.
155-
bool FindTableIndex(const COLORREF Color,
156-
const std::basic_string_view<COLORREF> ColorTable,
157-
_Out_ WORD* const pFoundIndex)
158-
{
159-
*pFoundIndex = 0;
160-
for (WORD i = 0; i < ColorTable.size(); i++)
161-
{
162-
if (ColorTable[i] == Color)
163-
{
164-
*pFoundIndex = i;
165-
return true;
166-
}
167-
}
168-
return false;
169-
}

src/host/server.h

+1-4
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ Revision History:
2929

3030
#include "..\host\RenderData.hpp"
3131

32-
#include "..\inc\IDefaultColorProvider.hpp"
33-
3432
// clang-format off
3533
// Flags flags
3634
#define CONSOLE_IS_ICONIC 0x00000001
@@ -75,8 +73,7 @@ class CommandHistory;
7573

7674
class CONSOLE_INFORMATION :
7775
public Settings,
78-
public Microsoft::Console::IIoProvider,
79-
public Microsoft::Console::IDefaultColorProvider
76+
public Microsoft::Console::IIoProvider
8077
{
8178
public:
8279
CONSOLE_INFORMATION();

src/host/ut_host/ConptyOutputTests.cpp

+1-3
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,7 @@ class ConptyOutputTests
8585
Viewport initialViewport = currentBuffer.GetViewport();
8686

8787
auto vtRenderEngine = std::make_unique<Xterm256Engine>(std::move(hFile),
88-
gci,
89-
initialViewport,
90-
gci.Get16ColorTable());
88+
initialViewport);
9189
auto pfn = std::bind(&ConptyOutputTests::_writeCallback, this, std::placeholders::_1, std::placeholders::_2);
9290
vtRenderEngine->SetTestCallback(pfn);
9391

0 commit comments

Comments
 (0)