terminal/src/buffer/out/TextAttribute.cpp
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

446 lines
15 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "TextAttribute.hpp"
#include "../../inc/conattrs.hpp"
// Keeping TextColor compact helps us keeping TextAttribute compact,
// which in turn ensures that our buffer memory usage is low.
static_assert(sizeof(TextAttribute) == 14);
static_assert(alignof(TextAttribute) == 2);
// Ensure that we can memcpy() and memmove() the struct for performance.
static_assert(std::is_trivially_copyable_v<TextAttribute>);
namespace
{
constexpr std::array<TextColor, 16> s_initLegacyColorMap(const BYTE defaultIndex)
{
std::array<TextColor, 16> legacyColorMap;
for (auto i = 0u; i < legacyColorMap.size(); i++)
{
const auto legacyIndex = TextColor::TransposeLegacyIndex(i);
gsl::at(legacyColorMap, i) = i == defaultIndex ? TextColor{} : TextColor{ legacyIndex, true };
}
return legacyColorMap;
}
BYTE s_legacyDefaultForeground = 7;
BYTE s_legacyDefaultBackground = 0;
BYTE s_ansiDefaultForeground = 7;
BYTE s_ansiDefaultBackground = 0;
}
// These maps allow for an efficient conversion from a legacy attribute index
// to a TextColor with the corresponding ANSI index, also taking into account
// the legacy index values that need to be converted to a default TextColor.
std::array<TextColor, 16> TextAttribute::s_legacyForegroundColorMap = s_initLegacyColorMap(7);
std::array<TextColor, 16> TextAttribute::s_legacyBackgroundColorMap = s_initLegacyColorMap(0);
// Routine Description:
// - Sets the legacy attributes which map to and from the default colors.
// Parameters:
// - defaultAttributes: the attribute values to be used for default colors.
// Return value:
// - None
void TextAttribute::SetLegacyDefaultAttributes(const WORD defaultAttributes) noexcept
{
// First we reset the current default color map entries to what they should
// be for a regular translation from a legacy index to an ANSI TextColor.
gsl::at(s_legacyForegroundColorMap, s_legacyDefaultForeground) = TextColor{ s_ansiDefaultForeground, true };
gsl::at(s_legacyBackgroundColorMap, s_legacyDefaultBackground) = TextColor{ s_ansiDefaultBackground, true };
// Then we save the new default attribute values and their corresponding
// ANSI translations. We use the latter values to more efficiently handle
// the "VT Quirk" conversion below.
s_legacyDefaultForeground = defaultAttributes & FG_ATTRS;
s_legacyDefaultBackground = (defaultAttributes & BG_ATTRS) >> 4;
s_ansiDefaultForeground = TextColor::TransposeLegacyIndex(s_legacyDefaultForeground);
s_ansiDefaultBackground = TextColor::TransposeLegacyIndex(s_legacyDefaultBackground);
// Finally we set the new default color map entries.
gsl::at(s_legacyForegroundColorMap, s_legacyDefaultForeground) = TextColor{};
gsl::at(s_legacyBackgroundColorMap, s_legacyDefaultBackground) = TextColor{};
}
// Routine Description:
// Pursuant to GH#6807
// This routine replaces VT colors from the 16-color set with the "default"
// flag. It is intended to be used as part of the "VT Quirk" in
// WriteConsole[AW].
//
// There is going to be a very long tail of applications that will
// explicitly request VT SGR 40/37 when what they really want is to
// SetConsoleTextAttribute() with a black background/white foreground.
// Instead of making those applications look bad (and therefore making us
// look bad, because we're releasing this as an update to something that
// "looks good" already), we're introducing this compatibility hack. Before
// the color reckoning in GH#6698 + GH#6506, *every* color was subject to
// being spontaneously and erroneously turned into the default color. Now,
// only the 16-color palette value that matches the active console
// background color will be destroyed when the quirk is enabled.
//
// This is not intended to be a long-term solution. This comment will be
// discovered in forty years(*) time and people will laugh at our hubris.
//
// *it doesn't matter when you're reading this, it will always be 40 years
// from now.
TextAttribute TextAttribute::StripErroneousVT16VersionsOfLegacyDefaults(const TextAttribute& attribute) noexcept
{
const auto fg{ attribute.GetForeground() };
const auto bg{ attribute.GetBackground() };
auto copy{ attribute };
if (fg.IsIndex16() &&
attribute.IsBold() == WI_IsFlagSet(s_ansiDefaultForeground, FOREGROUND_INTENSITY) &&
fg.GetIndex() == (s_ansiDefaultForeground & ~FOREGROUND_INTENSITY))
{
// We don't want to turn 1;37m into 39m (or even 1;39m), as this was meant to mimic a legacy color.
copy.SetDefaultForeground();
}
if (bg.IsIndex16() && bg.GetIndex() == s_ansiDefaultBackground)
{
copy.SetDefaultBackground();
}
return copy;
}
// Routine Description:
// - Returns a WORD with legacy-style attributes for this textattribute.
// Parameters:
// - None
// Return value:
// - a WORD with legacy-style attributes for this textattribute.
WORD TextAttribute::GetLegacyAttributes() const noexcept
{
const BYTE fgIndex = _foreground.GetLegacyIndex(s_legacyDefaultForeground);
const BYTE bgIndex = _background.GetLegacyIndex(s_legacyDefaultBackground);
const WORD metaAttrs = _wAttrLegacy & META_ATTRS;
const bool brighten = IsBold() && _foreground.CanBeBrightened();
return fgIndex | (bgIndex << 4) | metaAttrs | (brighten ? FOREGROUND_INTENSITY : 0);
}
bool TextAttribute::IsLegacy() const noexcept
{
return _foreground.IsLegacy() && _background.IsLegacy();
}
// Routine Description:
// - Calculates rgb colors based off of current color table and active modification attributes.
// Arguments:
// - colorTable: the current color table rgb values.
// - defaultFgIndex: the color table index of the default foreground color.
// - defaultBgIndex: the color table index of the default background color.
// - reverseScreenMode: true if the screen mode is reversed.
// - blinkingIsFaint: true if blinking should be interpreted as faint. (defaults to false)
// - boldIsBright: true if "bold" should be interpreted as bright. (defaults to true)
// Return Value:
// - the foreground and background colors that should be displayed.
std::pair<COLORREF, COLORREF> TextAttribute::CalculateRgbColors(const std::array<COLORREF, TextColor::TABLE_SIZE>& colorTable,
const size_t defaultFgIndex,
const size_t defaultBgIndex,
const bool reverseScreenMode,
const bool blinkingIsFaint,
const bool boldIsBright) const noexcept
{
auto fg = _foreground.GetColor(colorTable, defaultFgIndex, boldIsBright && IsBold());
auto bg = _background.GetColor(colorTable, defaultBgIndex);
if (IsFaint() || (IsBlinking() && blinkingIsFaint))
{
fg = (fg >> 1) & 0x7F7F7F; // Divide foreground color components by two.
}
if (IsReverseVideo() ^ reverseScreenMode)
{
std::swap(fg, bg);
}
if (IsInvisible())
{
fg = bg;
}
return { fg, bg };
}
// Method description:
// - Tells us whether the text is a hyperlink or not
// Return value:
// - True if it is a hyperlink, false otherwise
bool TextAttribute::IsHyperlink() const noexcept
{
// All non-hyperlink text have a default hyperlinkId of 0 while
// all hyperlink text have a non-zero hyperlinkId
return _hyperlinkId != 0;
}
TextColor TextAttribute::GetForeground() const noexcept
{
return _foreground;
}
TextColor TextAttribute::GetBackground() const noexcept
{
return _background;
}
// Method description:
// - Retrieves the hyperlink ID of the text
// Return value:
// - The hyperlink ID
uint16_t TextAttribute::GetHyperlinkId() const noexcept
{
return _hyperlinkId;
}
void TextAttribute::SetForeground(const TextColor foreground) noexcept
{
_foreground = foreground;
}
void TextAttribute::SetBackground(const TextColor background) noexcept
{
_background = background;
}
void TextAttribute::SetForeground(const COLORREF rgbForeground) noexcept
{
_foreground = TextColor(rgbForeground);
}
void TextAttribute::SetBackground(const COLORREF rgbBackground) noexcept
{
_background = TextColor(rgbBackground);
}
void TextAttribute::SetIndexedForeground(const BYTE fgIndex) noexcept
{
_foreground = TextColor(fgIndex, false);
}
void TextAttribute::SetIndexedBackground(const BYTE bgIndex) noexcept
{
_background = TextColor(bgIndex, false);
}
void TextAttribute::SetIndexedForeground256(const BYTE fgIndex) noexcept
{
_foreground = TextColor(fgIndex, true);
}
void TextAttribute::SetIndexedBackground256(const BYTE bgIndex) noexcept
{
_background = TextColor(bgIndex, true);
}
void TextAttribute::SetColor(const COLORREF rgbColor, const bool fIsForeground) noexcept
{
if (fIsForeground)
{
SetForeground(rgbColor);
}
else
{
SetBackground(rgbColor);
}
}
// Method description:
// - Sets the hyperlink ID of the text
// Arguments:
// - id - the id we wish to set
void TextAttribute::SetHyperlinkId(uint16_t id) noexcept
{
_hyperlinkId = id;
}
bool TextAttribute::IsLeadingByte() const noexcept
{
return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_LEADING_BYTE);
}
bool TextAttribute::IsTrailingByte() const noexcept
{
return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_LEADING_BYTE);
}
bool TextAttribute::IsTopHorizontalDisplayed() const noexcept
{
return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_GRID_HORIZONTAL);
}
bool TextAttribute::IsBottomHorizontalDisplayed() const noexcept
{
return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_UNDERSCORE);
}
bool TextAttribute::IsLeftVerticalDisplayed() const noexcept
{
return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_GRID_LVERTICAL);
}
bool TextAttribute::IsRightVerticalDisplayed() const noexcept
{
return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_GRID_RVERTICAL);
}
void TextAttribute::SetLeftVerticalDisplayed(const bool isDisplayed) noexcept
{
WI_UpdateFlag(_wAttrLegacy, COMMON_LVB_GRID_LVERTICAL, isDisplayed);
}
void TextAttribute::SetRightVerticalDisplayed(const bool isDisplayed) noexcept
{
WI_UpdateFlag(_wAttrLegacy, COMMON_LVB_GRID_RVERTICAL, isDisplayed);
}
bool TextAttribute::IsBold() const noexcept
{
return WI_IsFlagSet(_extendedAttrs, ExtendedAttributes::Bold);
}
bool TextAttribute::IsFaint() const noexcept
{
return WI_IsFlagSet(_extendedAttrs, ExtendedAttributes::Faint);
}
bool TextAttribute::IsItalic() const noexcept
{
return WI_IsFlagSet(_extendedAttrs, ExtendedAttributes::Italics);
}
bool TextAttribute::IsBlinking() const noexcept
{
return WI_IsFlagSet(_extendedAttrs, ExtendedAttributes::Blinking);
}
bool TextAttribute::IsInvisible() const noexcept
{
return WI_IsFlagSet(_extendedAttrs, ExtendedAttributes::Invisible);
}
bool TextAttribute::IsCrossedOut() const noexcept
{
return WI_IsFlagSet(_extendedAttrs, ExtendedAttributes::CrossedOut);
}
bool TextAttribute::IsUnderlined() const noexcept
{
return WI_IsFlagSet(_extendedAttrs, ExtendedAttributes::Underlined);
}
bool TextAttribute::IsDoublyUnderlined() const noexcept
{
return WI_IsFlagSet(_extendedAttrs, ExtendedAttributes::DoublyUnderlined);
}
bool TextAttribute::IsOverlined() const noexcept
{
return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_GRID_HORIZONTAL);
}
bool TextAttribute::IsReverseVideo() const noexcept
{
return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_REVERSE_VIDEO);
}
void TextAttribute::SetBold(bool isBold) noexcept
{
WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::Bold, isBold);
}
void TextAttribute::SetFaint(bool isFaint) noexcept
{
WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::Faint, isFaint);
}
void TextAttribute::SetItalic(bool isItalic) noexcept
{
WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::Italics, isItalic);
}
void TextAttribute::SetBlinking(bool isBlinking) noexcept
{
WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::Blinking, isBlinking);
}
void TextAttribute::SetInvisible(bool isInvisible) noexcept
{
WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::Invisible, isInvisible);
}
void TextAttribute::SetCrossedOut(bool isCrossedOut) noexcept
{
WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::CrossedOut, isCrossedOut);
}
void TextAttribute::SetUnderlined(bool isUnderlined) noexcept
{
WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::Underlined, isUnderlined);
}
void TextAttribute::SetDoublyUnderlined(bool isDoublyUnderlined) noexcept
{
WI_UpdateFlag(_extendedAttrs, ExtendedAttributes::DoublyUnderlined, isDoublyUnderlined);
}
void TextAttribute::SetOverlined(bool isOverlined) noexcept
{
WI_UpdateFlag(_wAttrLegacy, COMMON_LVB_GRID_HORIZONTAL, isOverlined);
}
void TextAttribute::SetReverseVideo(bool isReversed) noexcept
{
WI_UpdateFlag(_wAttrLegacy, COMMON_LVB_REVERSE_VIDEO, isReversed);
}
ExtendedAttributes TextAttribute::GetExtendedAttributes() const noexcept
{
return _extendedAttrs;
}
// Routine Description:
// - swaps foreground and background color
void TextAttribute::Invert() noexcept
{
WI_ToggleFlag(_wAttrLegacy, COMMON_LVB_REVERSE_VIDEO);
}
void TextAttribute::SetDefaultForeground() noexcept
{
_foreground = TextColor();
}
void TextAttribute::SetDefaultBackground() noexcept
{
_background = TextColor();
}
// Method description:
// - Resets only the meta and extended attributes
void TextAttribute::SetDefaultMetaAttrs() noexcept
{
_extendedAttrs = ExtendedAttributes::Normal;
_wAttrLegacy = 0;
}
// Method Description:
// - Returns true if this attribute indicates its background is the "default"
// background. Its _rgbBackground will contain the actual value of the
// default background. If the default colors are ever changed, this method
// should be used to identify attributes with the default bg value, and
// update them accordingly.
// Arguments:
// - <none>
// Return Value:
// - true iff this attribute indicates it's the "default" background color.
bool TextAttribute::BackgroundIsDefault() const noexcept
{
return _background.IsDefault();
}
// Routine Description:
// - Resets the meta and extended attributes, which is what the VT standard
// requires for most erasing and filling operations.
void TextAttribute::SetStandardErase() noexcept
{
SetDefaultMetaAttrs();
_hyperlinkId = 0;
}