Put the final touches on GDI's underlines (#16475)

While #16444 left wavy lines in an amazing state already, there were
a few more things that could be done to make GDI look more consistent
with other well known Windows applications.

But before that, a couple unrelated, but helpful changes were made:
* `GdiEngine::UpdateFont` was heavily modified to do all calculations
  in floats. All modern CPUs have fast FPUs and even the fairly slow
  `lroundf` function is so fast (relatively) nowadays that in a cold
  path like this, we can liberally call it to convert back to `int`s.
  This makes intermediate calculation more accurate and consistent.
* `GdiEngine::PaintBufferGridLines` was exception-unsafe due to its
  use of a `std::vector` with catch clause and this PR fixes that.
  Additionally, the vector was swapped out with a `til::small_vector`
  to reduce heap allocations. (Arena allocators!)
* RenderingTests was updated to cover styled underlines

With that in place, these improvements were done:
* Word's double-underline algorithm was ported over from `AtlasEngine`.
  It uses a half underline-width (aka `thinLineWidth`) which will now
  also be used for wavy lines to make them look a bit more filigrane.
* The Bézier curve for wavy/curly underlines was modified to use
  control points at (0.5,0.5) and (0.5,-0.5) respectively. This results
  in a maxima at y=0.1414 which is much closer to a sine curve with a
  maxima at 1/(2pi) = 0.1592. Previously, the maxima was a lot higher
  (roughly 4x) depending on the aspect ratio of the glyphs.
* Wavy underlines don't depend on the aspect ratio of glyphs anymore.
  This previously led to several problems depending on the exact font.
  The old renderer would draw exactly 3 periods of the wave into
  each cell which would also ensure continuity between cells.
  Unfortunately, this meant that waves could look inconsistent.
  The new approach always uses the aforementioned sine-like waves.
* The wavy underline offset was clamped so that it's never outside of
  bounds of a line. This avoids clipping.

## Validation Steps Performed
* Compile RenderingTests and run it
* Using Consolas, MS Gothic and Cascadia Code while Ctrl+Scrolling
  up and down works as expected without clipping 
This commit is contained in:
Leonard Hecker 2023-12-16 00:02:24 +01:00 committed by GitHub
parent 28acc102a5
commit 99193c9a3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 249 additions and 154 deletions

View File

@ -294,7 +294,6 @@ CPPCORECHECK
cppcorecheckrules
cpprestsdk
cppwinrt
CProc
cpx
CREATESCREENBUFFER
CREATESTRUCT
@ -324,7 +323,6 @@ CTRLVOLUME
Ctxt
CUF
cupxy
curlyline
CURRENTFONT
currentmode
CURRENTPAGE
@ -583,7 +581,6 @@ EXETYPE
exeuwp
exewin
exitwin
expectedinput
EXPUNGECOMMANDHISTORY
EXSTYLE
EXTENDEDEDITKEY
@ -795,7 +792,6 @@ HPA
hpcon
HPCON
hpen
hpj
HPR
HProvider
HREDRAW
@ -978,7 +974,6 @@ logissue
losslessly
loword
lparam
LPCCH
lpch
LPCPLINFO
LPCREATESTRUCT
@ -1177,7 +1172,6 @@ NOMOVE
NONALERT
nonbreaking
nonclient
NONCONST
NONINFRINGEMENT
NONPREROTATED
nonspace
@ -1525,17 +1519,14 @@ rgbs
rgci
rgfae
rgfte
rgi
rgn
rgp
rgpwsz
rgrc
rgui
rgw
RIGHTALIGN
RIGHTBUTTON
riid
Rike
RIS
roadmap
robomac
@ -1685,7 +1676,6 @@ SOLIDBOX
Solutiondir
somefile
sourced
spammy
SRCCODEPAGE
SRCCOPY
SRCINVERT
@ -1831,7 +1821,6 @@ Tpp
Tpqrst
tracelog
tracelogging
traceloggingprovider
traceviewpp
trackbar
TRACKCOMPOSITION
@ -1915,7 +1904,6 @@ USEFILLATTRIBUTE
USEGLYPHCHARS
USEHICON
USEPOSITION
userbase
USERDATA
userdpiapi
Userp
@ -1956,8 +1944,6 @@ vpack
vpackdirectory
VPACKMANIFESTDIRECTORY
VPR
VProc
VRaw
VREDRAW
vsc
vsconfig
@ -2001,7 +1987,6 @@ WCIA
WCIW
WCSHELPER
wcsicmp
wcsnicmp
wcsrev
wddm
wddmcon

View File

@ -117,12 +117,16 @@ namespace Microsoft::Console::Render
struct LineMetrics
{
int gridlineWidth;
int underlineOffset;
int underlineOffset2;
int thinLineWidth;
int underlineCenter;
int underlineWidth;
int doubleUnderlinePosTop;
int doubleUnderlinePosBottom;
int strikethroughOffset;
int strikethroughWidth;
int curlylinePeakHeight;
int curlyLineCenter;
int curlyLinePeriod;
int curlyLineControlPointOffset;
};
LineMetrics _lineMetrics;

View File

@ -2,9 +2,10 @@
// Licensed under the MIT license.
#include "precomp.h"
#include <vector>
#include "gdirenderer.hpp"
#include <til/small_vector.h>
#include "../inc/unicode.hpp"
#pragma hdrstop
@ -516,6 +517,7 @@ bool GdiEngine::FontHasWesternScript(HDC hdc)
// Return Value:
// - S_OK or suitable GDI HRESULT error or E_FAIL for GDI errors in functions that don't reliably return a specific error code.
[[nodiscard]] HRESULT GdiEngine::PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept
try
{
LOG_IF_FAILED(_FlushBufferLines());
@ -531,38 +533,50 @@ bool GdiEngine::FontHasWesternScript(HDC hdc)
// Get the font size so we know the size of the rectangle lines we'll be inscribing.
const auto fontWidth = _GetFontSize().width;
const auto fontHeight = _GetFontSize().height;
const auto widthOfAllCells = fontWidth * gsl::narrow_cast<unsigned>(cchLine);
const auto widthOfAllCells = fontWidth * gsl::narrow_cast<til::CoordType>(cchLine);
const auto DrawLine = [=](const auto x, const auto y, const auto w, const auto h) {
const auto DrawLine = [=](const til::CoordType x, const til::CoordType y, const til::CoordType w, const til::CoordType h) {
return PatBlt(_hdcMemoryContext, x, y, w, h, PATCOPY);
};
const auto DrawStrokedLine = [&](const til::CoordType x, const til::CoordType y, const unsigned w) {
const auto DrawStrokedLine = [&](const til::CoordType x, const til::CoordType y, const til::CoordType w) {
RETURN_HR_IF(E_FAIL, !MoveToEx(_hdcMemoryContext, x, y, nullptr));
RETURN_HR_IF(E_FAIL, !LineTo(_hdcMemoryContext, gsl::narrow_cast<int>(x + w), y));
RETURN_HR_IF(E_FAIL, !LineTo(_hdcMemoryContext, x + w, y));
return S_OK;
};
const auto DrawCurlyLine = [&](const til::CoordType x, const til::CoordType y, const size_t cCurlyLines) {
const auto curlyLineWidth = fontWidth;
const auto curlyLineHalfWidth = std::lround(curlyLineWidth / 2.0f);
const auto controlPointHeight = std::lround(3.5f * _lineMetrics.curlylinePeakHeight);
const auto DrawCurlyLine = [&](const til::CoordType begX, const til::CoordType y, const til::CoordType width) {
const auto period = _lineMetrics.curlyLinePeriod;
const auto halfPeriod = period / 2;
const auto controlPointOffset = _lineMetrics.curlyLineControlPointOffset;
// Each curlyLine requires 3 `POINT`s
const auto cPoints = gsl::narrow<DWORD>(3 * cCurlyLines);
std::vector<POINT> points;
points.reserve(cPoints);
// To ensure proper continuity of the wavy line between cells of different line color
// this code starts/ends the line earlier/later than it should and then clips it.
// Clipping in GDI is expensive, but it was the easiest approach.
// I've noticed that subtracting -1px prevents missing pixels when GDI draws. They still
// occur at certain (small) font sizes, but I couldn't figure out how to prevent those.
const auto lineStart = ((begX - 1) / period) * period;
const auto lineEnd = begX + width;
auto start = x;
for (size_t i = 0; i < cCurlyLines; i++)
IntersectClipRect(_hdcMemoryContext, begX, ptTarget.y, begX + width, ptTarget.y + fontHeight);
const auto restoreRegion = wil::scope_exit([&]() {
// Luckily no one else uses clip regions. They're weird to use.
SelectClipRgn(_hdcMemoryContext, nullptr);
});
// You can assume that each cell has roughly 5 POINTs on average. 128 POINTs is 1KiB.
til::small_vector<POINT, 128> points;
// This is the start point of the Bézier curve.
points.emplace_back(lineStart, y);
for (auto x = lineStart; x < lineEnd; x += period)
{
points.emplace_back(start + curlyLineHalfWidth, y - controlPointHeight);
points.emplace_back(start + curlyLineHalfWidth, y + controlPointHeight);
points.emplace_back(start + curlyLineWidth, y);
start += curlyLineWidth;
points.emplace_back(x + halfPeriod, y - controlPointOffset);
points.emplace_back(x + halfPeriod, y + controlPointOffset);
points.emplace_back(x + period, y);
}
RETURN_HR_IF(E_FAIL, !MoveToEx(_hdcMemoryContext, x, y, nullptr));
RETURN_HR_IF(E_FAIL, !PolyBezierTo(_hdcMemoryContext, points.data(), cPoints));
return S_OK;
const auto cpt = gsl::narrow_cast<DWORD>(points.size());
return PolyBezier(_hdcMemoryContext, points.data(), cpt);
};
if (lines.test(GridLines::Left))
@ -605,7 +619,6 @@ bool GdiEngine::FontHasWesternScript(HDC hdc)
RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.strikethroughWidth));
}
// Create a pen matching the underline style.
DWORD underlinePenType = PS_SOLID;
if (lines.test(GridLines::DottedUnderline))
{
@ -615,8 +628,15 @@ bool GdiEngine::FontHasWesternScript(HDC hdc)
{
underlinePenType = PS_DASH;
}
DWORD underlineWidth = _lineMetrics.underlineWidth;
if (lines.any(GridLines::DoubleUnderline, GridLines::CurlyUnderline))
{
underlineWidth = _lineMetrics.thinLineWidth;
}
const LOGBRUSH brushProp{ .lbStyle = BS_SOLID, .lbColor = underlineColor };
wil::unique_hpen hpen(ExtCreatePen(underlinePenType | PS_GEOMETRIC | PS_ENDCAP_FLAT, _lineMetrics.underlineWidth, &brushProp, 0, nullptr));
wil::unique_hpen hpen(ExtCreatePen(underlinePenType | PS_GEOMETRIC | PS_ENDCAP_FLAT, underlineWidth, &brushProp, 0, nullptr));
// Apply the pen.
const auto prevPen = wil::SelectObject(_hdcMemoryContext, hpen.get());
@ -624,28 +644,29 @@ bool GdiEngine::FontHasWesternScript(HDC hdc)
if (lines.test(GridLines::Underline))
{
return DrawStrokedLine(ptTarget.x, ptTarget.y + _lineMetrics.underlineOffset, widthOfAllCells);
return DrawStrokedLine(ptTarget.x, ptTarget.y + _lineMetrics.underlineCenter, widthOfAllCells);
}
else if (lines.test(GridLines::DoubleUnderline))
{
RETURN_IF_FAILED(DrawStrokedLine(ptTarget.x, ptTarget.y + _lineMetrics.underlineOffset, widthOfAllCells));
return DrawStrokedLine(ptTarget.x, ptTarget.y + _lineMetrics.underlineOffset2, widthOfAllCells);
RETURN_IF_FAILED(DrawStrokedLine(ptTarget.x, ptTarget.y + _lineMetrics.doubleUnderlinePosTop, widthOfAllCells));
return DrawStrokedLine(ptTarget.x, ptTarget.y + _lineMetrics.doubleUnderlinePosBottom, widthOfAllCells);
}
else if (lines.test(GridLines::CurlyUnderline))
{
return DrawCurlyLine(ptTarget.x, ptTarget.y + _lineMetrics.underlineOffset, cchLine);
return DrawCurlyLine(ptTarget.x, ptTarget.y + _lineMetrics.curlyLineCenter, widthOfAllCells);
}
else if (lines.test(GridLines::DottedUnderline))
{
return DrawStrokedLine(ptTarget.x, ptTarget.y + _lineMetrics.underlineOffset, widthOfAllCells);
return DrawStrokedLine(ptTarget.x, ptTarget.y + _lineMetrics.underlineCenter, widthOfAllCells);
}
else if (lines.test(GridLines::DashedUnderline))
{
return DrawStrokedLine(ptTarget.x, ptTarget.y + _lineMetrics.underlineOffset, widthOfAllCells);
return DrawStrokedLine(ptTarget.x, ptTarget.y + _lineMetrics.underlineCenter, widthOfAllCells);
}
return S_OK;
}
CATCH_RETURN();
// Routine Description:
// - Draws the cursor on the screen

View File

@ -344,85 +344,109 @@ GdiEngine::~GdiEngine()
// There is no font metric for the grid line width, so we use a small
// multiple of the font size, which typically rounds to a pixel.
const auto fontSize = _tmFontMetrics.tmHeight - _tmFontMetrics.tmInternalLeading;
_lineMetrics.gridlineWidth = std::lround(fontSize * 0.025);
const auto cellHeight = static_cast<float>(Font.GetSize().height);
const auto fontSize = static_cast<float>(_tmFontMetrics.tmHeight - _tmFontMetrics.tmInternalLeading);
const auto baseline = static_cast<float>(_tmFontMetrics.tmAscent);
float idealGridlineWidth = std::max(1.0f, fontSize * 0.025f);
float idealUnderlineTop = 0;
float idealUnderlineWidth = 0;
float idealStrikethroughTop = 0;
float idealStrikethroughWidth = 0;
OUTLINETEXTMETRICW outlineMetrics;
if (GetOutlineTextMetricsW(_hdcMemoryContext, sizeof(outlineMetrics), &outlineMetrics))
{
// For TrueType fonts, the other line metrics can be obtained from
// the font's outline text metric structure.
_lineMetrics.underlineOffset = outlineMetrics.otmsUnderscorePosition;
_lineMetrics.underlineWidth = outlineMetrics.otmsUnderscoreSize;
_lineMetrics.strikethroughOffset = outlineMetrics.otmsStrikeoutPosition;
_lineMetrics.strikethroughWidth = outlineMetrics.otmsStrikeoutSize;
idealUnderlineTop = static_cast<float>(baseline - outlineMetrics.otmsUnderscorePosition);
idealUnderlineWidth = static_cast<float>(outlineMetrics.otmsUnderscoreSize);
idealStrikethroughWidth = static_cast<float>(outlineMetrics.otmsStrikeoutSize);
idealStrikethroughTop = static_cast<float>(baseline - outlineMetrics.otmsStrikeoutPosition);
}
else
{
// If we can't obtain the outline metrics for the font, we just pick
// some reasonable values for the offsets and widths.
_lineMetrics.underlineOffset = -std::lround(fontSize * 0.05);
_lineMetrics.underlineWidth = _lineMetrics.gridlineWidth;
_lineMetrics.strikethroughOffset = std::lround(_tmFontMetrics.tmAscent / 3.0);
_lineMetrics.strikethroughWidth = _lineMetrics.gridlineWidth;
// If we can't obtain the outline metrics for the font, we just pick some reasonable values for the offsets and widths.
idealUnderlineTop = std::max(1.0f, roundf(baseline - fontSize * 0.05f));
idealUnderlineWidth = idealGridlineWidth;
idealStrikethroughTop = std::max(1.0f, roundf(baseline * (2.0f / 3.0f)));
idealStrikethroughWidth = idealGridlineWidth;
}
// We always want the lines to be visible, so if a stroke width ends
// up being zero, we need to make it at least 1 pixel.
_lineMetrics.gridlineWidth = std::max(_lineMetrics.gridlineWidth, 1);
_lineMetrics.underlineWidth = std::max(_lineMetrics.underlineWidth, 1);
_lineMetrics.strikethroughWidth = std::max(_lineMetrics.strikethroughWidth, 1);
// GdiEngine::PaintBufferGridLines paints underlines using HPEN and LineTo, etc., which draws lines centered on the given coordinates.
// This means we need to shift the limit (cellHeight - underlineWidth) and offset (idealUnderlineTop) by half the width.
const auto underlineWidth = std::max(1.0f, roundf(idealUnderlineWidth));
const auto underlineCenter = std::min(floorf(cellHeight - underlineWidth / 2.0f), roundf(idealUnderlineTop + underlineWidth / 2.0f));
// Offsets are relative to the base line of the font, so we subtract
// from the ascent to get an offset relative to the top of the cell.
const auto ascent = _tmFontMetrics.tmAscent;
_lineMetrics.underlineOffset = ascent - _lineMetrics.underlineOffset;
_lineMetrics.strikethroughOffset = ascent - _lineMetrics.strikethroughOffset;
const auto strikethroughWidth = std::max(1.0f, roundf(idealStrikethroughWidth));
const auto strikethroughOffset = std::min(cellHeight - strikethroughWidth, roundf(idealStrikethroughTop));
// For double underlines we need a second offset, just below the first,
// but with a bit of a gap (about double the grid line width).
_lineMetrics.underlineOffset2 = _lineMetrics.underlineOffset +
_lineMetrics.underlineWidth +
std::lround(fontSize * 0.05);
// For double underlines we loosely follow what Word does:
// 1. The lines are half the width of an underline
// 2. Ideally the bottom line is aligned with the bottom of the underline
// 3. The top underline is vertically in the middle between baseline and ideal bottom underline
// 4. If the top line gets too close to the baseline the underlines are shifted downwards
// 5. The minimum gap between the two lines appears to be similar to Tex (1.2pt)
// (Additional notes below.)
// However, we don't want the underline to extend past the bottom of the
// cell, so we clamp the offset to fit just inside.
const auto maxUnderlineOffset = Font.GetSize().height - _lineMetrics.underlineWidth;
_lineMetrics.underlineOffset2 = std::min(_lineMetrics.underlineOffset2, maxUnderlineOffset);
// 1.
const auto thinLineWidth = std::max(1.0f, roundf(idealUnderlineWidth / 2.0f));
// 2.
auto doubleUnderlinePosBottom = underlineCenter + underlineWidth - thinLineWidth;
// 3. Since we don't align the center of our two lines, but rather the top borders
// we need to subtract half a line width from our center point.
auto doubleUnderlinePosTop = roundf((baseline + doubleUnderlinePosBottom - thinLineWidth) / 2.0f);
// 4.
doubleUnderlinePosTop = std::max(doubleUnderlinePosTop, baseline + thinLineWidth);
// 5. The gap is only the distance _between_ the lines, but we need the distance from the
// top border of the top and bottom lines, which includes an additional line width.
const auto doubleUnderlineGap = std::max(1.0f, roundf(1.2f / 72.0f * _iCurrentDpi));
doubleUnderlinePosBottom = std::max(doubleUnderlinePosBottom, doubleUnderlinePosTop + doubleUnderlineGap + thinLineWidth);
// Our cells can't overlap each other so we additionally clamp the bottom line to be inside the cell boundaries.
doubleUnderlinePosBottom = std::min(doubleUnderlinePosBottom, cellHeight - thinLineWidth);
// But if the resulting gap isn't big enough even to register as a thicker
// line, it's better to place the second line slightly above the first.
if (_lineMetrics.underlineOffset2 < _lineMetrics.underlineOffset + _lineMetrics.gridlineWidth)
{
_lineMetrics.underlineOffset2 = _lineMetrics.underlineOffset - _lineMetrics.gridlineWidth;
}
// The wave line is drawn using a cubic Bézier curve (PolyBezier), because that happens to be cheap with GDI.
// We use a Bézier curve where, if the start (a) and end (c) points are at (0,0) and (1,0), the control points are
// at (0.5,0.5) (b) and (0.5,-0.5) (d) respectively. Like this but a/b/c/d are square and the lines are round:
//
// b
//
// ^
// / \ here's some text so the compiler ignores the trailing \ character
// a \ c
// \ /
// v
//
// d
//
// If you punch x=0.25 into the cubic bezier formula you get y=0.140625. This constant is
// important to us because it (plus the line width) tells us the amplitude of the wave.
//
// We can use the inverse of the constant to figure out how many px one period of the wave has to be to end up being 1px tall.
// In our case we want the amplitude of the wave to have a peak-to-peak amplitude that matches our double-underline.
const auto doubleUnderlineHalfDistance = 0.5f * (doubleUnderlinePosBottom - doubleUnderlinePosTop);
const auto doubleUnderlineCenter = doubleUnderlinePosTop + doubleUnderlineHalfDistance;
const auto curlyLineIdealAmplitude = std::max(1.0f, doubleUnderlineHalfDistance);
// Since GDI can't deal with fractional pixels, we first calculate the control point offsets (0.5 and -0.5) by multiplying by 0.5 and
// then undo that by multiplying by 2.0 for the period. This ensures that our control points can be at curlyLinePeriod/2, an integer.
const auto curlyLineControlPointOffset = roundf(curlyLineIdealAmplitude * (1.0f / 0.140625f) * 0.5f);
const auto curlyLinePeriod = curlyLineControlPointOffset * 2.0f;
// We can reverse the above to get back the actual amplitude of our Bézier curve. The line
// will be drawn with a width of thinLineWidth in the center of the curve (= 0.5x padding).
const auto curlyLineAmplitude = 0.140625f * curlyLinePeriod + 0.5f * thinLineWidth;
// To make the wavy line with its double-underline amplitude look consistent with the double-underline we position it at its center.
const auto curlyLineOffset = std::min(roundf(doubleUnderlineCenter), floorf(cellHeight - curlyLineAmplitude));
// Since we use GDI pen for drawing, the underline offset should point to
// the center of the underline.
const auto underlineHalfWidth = gsl::narrow_cast<int>(std::floor(_lineMetrics.underlineWidth / 2.0f));
_lineMetrics.underlineOffset += underlineHalfWidth;
_lineMetrics.underlineOffset2 += underlineHalfWidth;
// Curlyline is drawn with a desired height relative to the font size. The
// baseline of curlyline is at the middle of singly underline. When there's
// limited space to draw a curlyline, we apply a limit on the peak height.
{
// initialize curlyline peak height to a desired value. Clamp it to at
// least 1.
constexpr auto curlyLinePeakHeightEm = 0.075f;
_lineMetrics.curlylinePeakHeight = gsl::narrow_cast<int>(std::max(1L, std::lround(curlyLinePeakHeightEm * fontSize)));
// calc the limit we need to apply
const auto maxDrawableCurlyLinePeakHeight = Font.GetSize().height - _lineMetrics.underlineOffset - _lineMetrics.underlineWidth;
// if the limit is <= 0 (no height at all), stick with the desired height.
// This is how we force a curlyline even when there's no space, though it
// might be clipped at the bottom.
if (maxDrawableCurlyLinePeakHeight > 0.0f)
{
_lineMetrics.curlylinePeakHeight = std::min(_lineMetrics.curlylinePeakHeight, maxDrawableCurlyLinePeakHeight);
}
}
_lineMetrics.gridlineWidth = lroundf(idealGridlineWidth);
_lineMetrics.thinLineWidth = lroundf(thinLineWidth);
_lineMetrics.underlineCenter = lroundf(underlineCenter);
_lineMetrics.underlineWidth = lroundf(underlineWidth);
_lineMetrics.doubleUnderlinePosTop = lroundf(doubleUnderlinePosTop);
_lineMetrics.doubleUnderlinePosBottom = lroundf(doubleUnderlinePosBottom);
_lineMetrics.strikethroughOffset = lroundf(strikethroughOffset);
_lineMetrics.strikethroughWidth = lroundf(strikethroughWidth);
_lineMetrics.curlyLineCenter = lroundf(curlyLineOffset);
_lineMetrics.curlyLinePeriod = lroundf(curlyLinePeriod);
_lineMetrics.curlyLineControlPointOffset = lroundf(curlyLineControlPointOffset);
// Now find the size of a 0 in this current font and save it for conversions done later.
_coordFontLast = Font.GetSize();

View File

@ -5,7 +5,39 @@
#include <array>
#include <cassert>
#include <cstdio>
#include <string_view>
// The following list of colors is only used as a debug aid and not part of the final product.
// They're licensed under:
//
// Apache-Style Software License for ColorBrewer software and ColorBrewer Color Schemes
//
// Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State University.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.
//
namespace colorbrewer
{
inline constexpr uint32_t pastel1[]{
0xfbb4ae,
0xb3cde3,
0xccebc5,
0xdecbe4,
0xfed9a6,
0xffffcc,
0xe5d8bd,
0xfddaec,
0xf2f2f2,
};
}
// Another variant of "defer" for C++.
namespace
@ -110,12 +142,14 @@ int main()
};
{
struct ConsoleAttributeTest
struct AttributeTest
{
const wchar_t* text = nullptr;
WORD attribute = 0;
};
static constexpr ConsoleAttributeTest consoleAttributeTests[]{
{
static constexpr AttributeTest consoleAttributeTests[]{
{ L"Console attributes:", 0 },
#define MAKE_TEST_FOR_ATTRIBUTE(attr) { L## #attr, attr }
MAKE_TEST_FOR_ATTRIBUTE(COMMON_LVB_GRID_HORIZONTAL),
@ -142,14 +176,10 @@ int main()
row += 2;
}
}
struct VTAttributeTest
{
const wchar_t* text = nullptr;
int sgr = 0;
};
static constexpr VTAttributeTest vtAttributeTests[]{
{ L"ANSI escape SGR:", 0 },
static constexpr AttributeTest basicSGR[]{
{ L"bold", 1 },
{ L"faint", 2 },
{ L"italic", 3 },
@ -160,14 +190,45 @@ int main()
{ L"overlined", 53 },
};
row = 3;
for (const auto& t : vtAttributeTests)
printfUTF16(L"\x1B[3;39HANSI escape SGR:");
int row = 5;
for (const auto& t : basicSGR)
{
printfUTF16(L"\x1B[%d;45H\x1b[%dm%s\x1b[m", row, t.sgr, t.text);
printfUTF16(L"\x1B[%d;39H\x1b[%dm%s\x1b[m", row, t.attribute, t.text);
row += 2;
}
printfUTF16(L"\x1B[%d;45H\x1b]8;;https://example.com\x1b\\hyperlink\x1b]8;;\x1b\\", row);
printfUTF16(L"\x1B[%d;39H\x1b]8;;https://example.com\x1b\\hyperlink\x1b]8;;\x1b\\", row);
}
{
static constexpr AttributeTest styledUnderlines[]{
{ L"straight", 1 },
{ L"double", 2 },
{ L"curly", 3 },
{ L"dotted", 4 },
{ L"dashed", 5 },
};
printfUTF16(L"\x1B[3;63HStyled Underlines:");
int row = 5;
for (const auto& t : styledUnderlines)
{
printfUTF16(L"\x1B[%d;63H\x1b[4:%dm", row, t.attribute);
const auto len = wcslen(t.text);
for (size_t i = 0; i < len; ++i)
{
const auto color = colorbrewer::pastel1[i % std::size(colorbrewer::pastel1)];
printfUTF16(L"\x1B[58:2::%d:%d:%dm%c", (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, t.text[i]);
}
printfUTF16(L"\x1b[m");
row += 2;
}
}
wait();
clear();