terminal/src/buffer/out/TextColor.h
James Holderness bb71179a24
Consolidate the color palette APIs (#11784)
This PR merges the default colors and cursor color into the main color
table, enabling us to simplify the `ConGetSet` and `ITerminalApi`
interfaces, with just two methods required for getting and setting any
form of color palette entry.

The is a follow-up to the color table standardization in #11602, and a
another small step towards de-duplicating `AdaptDispatch` and
`TerminalDispatch` for issue #3849. It should also make it easier to
support color queries (#3718) and a configurable bold color (#5682) in
the future.

On the conhost side, default colors could originally be either indexed
positions in the 16-color table, or separate standalone RGB values. With
the new system, the default colors will always be in the color table, so
we just need to track their index positions.

To make this work, those positions need to be calculated at startup
based on the loaded registry/shortcut settings, and updated when
settings are changed (this is handled in
`CalculateDefaultColorIndices`). But the plus side is that it's now much
easier to lookup the default color values for rendering.

For now the default colors in Windows Terminal use hardcoded positions,
because it doesn't need indexed default colors like conhost. But in the
future I'd like to extend the index handling to both terminals, so we
can eventually support the VT525 indexed color operations.

As for the cursor color, that was previously stored in the `Cursor`
class, which meant that it needed to be copied around in various places
where cursors were being instantiated. Now that it's managed separately
in the color table, a lot of that code is no longer required.

## Validation
Some of the unit test initialization code needed to be updated to setup
the color table and default index values as required for the new system.
There were also some adjustments needed to account for API changes, in
particular for methods that now take index values for the default colors
in place of COLORREFs. But for the most part, the essential behavior of
the tests remains unchanged.

I've also run a variety of manual tests looking at the legacy console
APIs as well as the various VT color sequences, and checking that
everything works as expected when color schemes are changed, both in
Windows Terminal and conhost, and in the latter case with both indexed
colors and RGB values.

Closes #11768
2021-11-23 18:28:55 +00:00

189 lines
5.8 KiB
C++

/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- TextColor.h
Abstract:
- contains data for a single color of the text. Text Attributes are composed of
two of these - one for the foreground and one for the background.
The color can be in one of three states:
* Default Colors - The terminal should use the terminal's notion of whatever
the default color should be for this component.
It's up to the terminal that's consuming this buffer to control the
behavior of default attributes.
Terminals typically have a pair of Default colors that are separate from
their color table. This component should use that value.
Consoles also can have a legacy table index as their default colors.
* Indexed Color - The terminal should use our value as an index into the
color table to retrieve the real value of the color.
This is the type of color that "legacy" 16-color attributes have.
* RGB color - We'll store a real color value in this attribute
Author(s):
- Mike Griese (migrie) Nov 2018
Revision History:
- From components of output.h/.c
by Therese Stowell (ThereseS) 1990-1991
- Pulled into its own file from textBuffer.hpp/cpp (AustDi, 2017)
- Moved the colors into their own separate abstraction. (migrie Nov 2018)
--*/
#pragma once
#ifdef UNIT_TESTING
#include "WexTestClass.h"
#endif
enum class ColorType : BYTE
{
IsIndex256 = 0x0,
IsIndex16 = 0x1,
IsDefault = 0x2,
IsRgb = 0x3
};
struct TextColor
{
public:
static constexpr BYTE DARK_BLACK = 0;
static constexpr BYTE DARK_RED = 1;
static constexpr BYTE DARK_GREEN = 2;
static constexpr BYTE DARK_YELLOW = 3;
static constexpr BYTE DARK_BLUE = 4;
static constexpr BYTE DARK_MAGENTA = 5;
static constexpr BYTE DARK_CYAN = 6;
static constexpr BYTE DARK_WHITE = 7;
static constexpr BYTE BRIGHT_BLACK = 8;
static constexpr BYTE BRIGHT_RED = 9;
static constexpr BYTE BRIGHT_GREEN = 10;
static constexpr BYTE BRIGHT_YELLOW = 11;
static constexpr BYTE BRIGHT_BLUE = 12;
static constexpr BYTE BRIGHT_MAGENTA = 13;
static constexpr BYTE BRIGHT_CYAN = 14;
static constexpr BYTE BRIGHT_WHITE = 15;
static constexpr size_t DEFAULT_FOREGROUND = 256;
static constexpr size_t DEFAULT_BACKGROUND = 257;
static constexpr size_t CURSOR_COLOR = 258;
static constexpr size_t TABLE_SIZE = 259;
constexpr TextColor() noexcept :
_meta{ ColorType::IsDefault },
_red{ 0 },
_green{ 0 },
_blue{ 0 }
{
}
constexpr TextColor(const BYTE index, const bool isIndex256) noexcept :
_meta{ isIndex256 ? ColorType::IsIndex256 : ColorType::IsIndex16 },
_index{ index },
_green{ 0 },
_blue{ 0 }
{
}
constexpr TextColor(const COLORREF rgb) noexcept :
_meta{ ColorType::IsRgb },
_red{ GetRValue(rgb) },
_green{ GetGValue(rgb) },
_blue{ GetBValue(rgb) }
{
}
friend constexpr bool operator==(const TextColor& a, const TextColor& b) noexcept;
friend constexpr bool operator!=(const TextColor& a, const TextColor& b) noexcept;
bool CanBeBrightened() const noexcept;
bool IsLegacy() const noexcept;
bool IsIndex16() const noexcept;
bool IsIndex256() const noexcept;
bool IsDefault() const noexcept;
bool IsRgb() const noexcept;
void SetColor(const COLORREF rgbColor) noexcept;
void SetIndex(const BYTE index, const bool isIndex256) noexcept;
void SetDefault() noexcept;
COLORREF GetColor(const std::array<COLORREF, TABLE_SIZE>& colorTable, const size_t defaultIndex, bool brighten = false) const noexcept;
BYTE GetLegacyIndex(const BYTE defaultIndex) const noexcept;
constexpr BYTE GetIndex() const noexcept
{
return _index;
}
COLORREF GetRGB() const noexcept;
static constexpr BYTE TransposeLegacyIndex(const size_t index)
{
// When converting a 16-color index in the legacy Windows order to or
// from an ANSI-compatible order, we need to swap the bits in positions
// 0 and 2. We do this by XORing the index with 00000101, but only if
// one (but not both) of those bit positions is set.
const auto oneBitSet = (index ^ (index >> 2)) & 1;
return gsl::narrow_cast<BYTE>(index ^ oneBitSet ^ (oneBitSet << 2));
}
private:
union
{
BYTE _red, _index;
};
BYTE _green;
BYTE _blue;
ColorType _meta;
#ifdef UNIT_TESTING
friend class TextBufferTests;
template<typename TextColor>
friend class WEX::TestExecution::VerifyOutputTraits;
#endif
};
bool constexpr operator==(const TextColor& a, const TextColor& b) noexcept
{
return a._meta == b._meta &&
a._red == b._red &&
a._green == b._green &&
a._blue == b._blue;
}
bool constexpr operator!=(const TextColor& a, const TextColor& b) noexcept
{
return !(a == b);
}
#ifdef UNIT_TESTING
namespace WEX
{
namespace TestExecution
{
template<>
class VerifyOutputTraits<TextColor>
{
public:
static WEX::Common::NoThrowString ToString(const TextColor& color)
{
if (color.IsDefault())
{
return L"{default}";
}
else if (color.IsRgb())
{
return WEX::Common::NoThrowString().Format(L"{RGB:0x%06x}", color.GetRGB());
}
else
{
return WEX::Common::NoThrowString().Format(L"{index:0x%04x}", color._red);
}
}
};
}
}
#endif