Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support rendering of underline style and color #16097

Merged
merged 24 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d2bea4c
initial commit for underline style and color
tusharsnx Oct 3, 2023
d341402
add new underline styles in UIA
tusharsnx Oct 9, 2023
a042401
draw curly line from the shader
tusharsnx Oct 12, 2023
e16055c
revert D2D antialias mode
tusharsnx Oct 12, 2023
1df5894
fix gridlines being drawn with underline color
tusharsnx Oct 14, 2023
0bdeb1a
add dotted underline to BackendD2D and Dx renderers
tusharsnx Oct 14, 2023
c179041
fix ScrollTest's MockScrollRenderEngine wasn't updated to receive und…
tusharsnx Oct 14, 2023
8684de8
make sine wave start with a crest
tusharsnx Oct 16, 2023
dc986c1
fix strikethrough being drawn with underline color
tusharsnx Oct 16, 2023
bea07a5
add support for underline styles in gdi renderer
tusharsnx Oct 24, 2023
4f13a3c
add spellings
tusharsnx Oct 24, 2023
1f3e535
fix signed/unsigned mismatch
tusharsnx Oct 25, 2023
2babe7b
make small improvements to Atlas curlyline rendering
tusharsnx Oct 25, 2023
e31d0fb
AtlasEngine: use cellBottomGap as the peak height for curly line
tusharsnx Oct 27, 2023
63ae6ce
GDIRenderer: use cellBottomGap as the peak height for curly line
tusharsnx Oct 27, 2023
b5574d0
initialize _curlyLineDrawPeakHeight
tusharsnx Oct 27, 2023
b8d3caa
send line rendition scale using texcoord
tusharsnx Oct 28, 2023
a2c4a36
underlines are mutually exclusive
tusharsnx Oct 28, 2023
6e4c65a
revert underline thickness scaling
tusharsnx Oct 28, 2023
b0be8b6
move curlyline peak height constants into the function
tusharsnx Nov 2, 2023
1ba1782
fix curlyline equation that was making a negative peak at the start
tusharsnx Nov 2, 2023
4f7ba10
BackendD2D: use `else if`
tusharsnx Nov 2, 2023
c7a8a55
shrink QuadInstance::ShadingType and add renditionScale member
tusharsnx Nov 3, 2023
3923fde
replace SelectObject with wil::SelectObject
tusharsnx Nov 4, 2023
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
22 changes: 19 additions & 3 deletions src/renderer/atlas/AtlasEngine.api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -686,12 +686,19 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
adjustedWidth = std::max(1.0f, adjustedWidth);
adjustedHeight = std::max(1.0f, adjustedHeight);

// TODO: This helps in testing the underlines at different underline
// widths, particularly curly line, so they don't break if/when we get
// support for user customizable underline width. Remove it before merging.
// I'm using 1.5f as a hardcoded value below. In the future, we would
// get it from the user/config.
const auto underlineWidthScale = std::clamp(1.0f, 1.5f, 2.0f);

const auto baseline = std::roundf(ascent + (lineGap + adjustedHeight - advanceHeight) / 2.0f);
const auto underlinePos = std::roundf(baseline + underlinePosition);
const auto underlineWidth = std::max(1.0f, std::roundf(underlineThickness));
const auto underlineWidth = std::max(1.0f, std::roundf(underlineThickness * underlineWidthScale));
const auto strikethroughPos = std::roundf(baseline + strikethroughPosition);
const auto strikethroughWidth = std::max(1.0f, std::roundf(strikethroughThickness));
const auto thinLineWidth = std::max(1.0f, std::roundf(underlineThickness / 2.0f));
const auto strikethroughWidth = std::max(1.0f, std::roundf(strikethroughThickness * underlineWidthScale));
const auto thinLineWidth = std::max(1.0f, std::roundf(underlineThickness * underlineWidthScale / 2.0f));

// For double underlines we loosely follow what Word does:
// 1. The lines are half the width of an underline (= thinLineWidth)
Expand All @@ -715,6 +722,12 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
// Our cells can't overlap each other so we additionally clamp the bottom line to be inside the cell boundaries.
doubleUnderlinePosBottom = std::min(doubleUnderlinePosBottom, adjustedHeight - thinLineWidth);

// For curly-line, we'll make room for the top and bottom curve ("wave").
// The baseline for curly-line is kept at the baseline of singly underline.
const auto curlyLinePeakHeight = std::roundf(fontMetrics->curlyUnderlineWaviness * fontSizeInPx);
const auto curlyUnderlinePos = underlinePos - (curlyLinePeakHeight + underlineThickness / 2.0f);
const auto curlyUnderlineWidth = underlineWidth + 2.0f * (curlyLinePeakHeight + underlineThickness / 2.0f);

const auto cellWidth = gsl::narrow<u16>(lrintf(adjustedWidth));
const auto cellHeight = gsl::narrow<u16>(lrintf(adjustedHeight));

Expand Down Expand Up @@ -748,6 +761,8 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo

const auto underlinePosU16 = gsl::narrow_cast<u16>(lrintf(underlinePos));
const auto underlineWidthU16 = gsl::narrow_cast<u16>(lrintf(underlineWidth));
const auto curlyUnderlinePosU16 = gsl::narrow_cast<u16>(lrintf(curlyUnderlinePos));
const auto curlyUnderlineWidthU16 = gsl::narrow_cast<u16>(lrintf(curlyUnderlineWidth));
const auto strikethroughPosU16 = gsl::narrow_cast<u16>(lrintf(strikethroughPos));
const auto strikethroughWidthU16 = gsl::narrow_cast<u16>(lrintf(strikethroughWidth));
const auto doubleUnderlinePosTopU16 = gsl::narrow_cast<u16>(lrintf(doubleUnderlinePosTop));
Expand All @@ -774,6 +789,7 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo

fontMetrics->underline = { underlinePosU16, underlineWidthU16 };
fontMetrics->strikethrough = { strikethroughPosU16, strikethroughWidthU16 };
fontMetrics->curlyUnderline = { curlyUnderlinePosU16, curlyUnderlineWidthU16 };
fontMetrics->doubleUnderline[0] = { doubleUnderlinePosTopU16, thinLineWidthU16 };
fontMetrics->doubleUnderline[1] = { doubleUnderlinePosBottomU16, thinLineWidthU16 };
fontMetrics->overline = { 0, underlineWidthU16 };
Expand Down
107 changes: 103 additions & 4 deletions src/renderer/atlas/BackendD3D.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,9 @@ void BackendD3D::_resetGlyphAtlas(const RenderingPayload& p)
_d2dBeginDrawing();
_d2dRenderTarget->Clear();

// (re)draw custom geometries
_drawCurlylineToAtlas(p);

_fontChangedResetGlyphAtlas = false;
}

Expand Down Expand Up @@ -772,9 +775,7 @@ void BackendD3D::_resizeGlyphAtlas(const RenderingPayload& p, const u16 u, const
_d2dRenderTarget.try_query_to(_d2dRenderTarget4.addressof());

_d2dRenderTarget->SetUnitMode(D2D1_UNIT_MODE_PIXELS);
// We don't really use D2D for anything except DWrite, but it
// can't hurt to ensure that everything it does is pixel aligned.
_d2dRenderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
_d2dRenderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
tusharsnx marked this conversation as resolved.
Show resolved Hide resolved
// Ensure that D2D uses the exact same gamma as our shader uses.
_d2dRenderTarget->SetTextRenderingParams(_textRenderingParams.get());

Expand Down Expand Up @@ -1644,6 +1645,7 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y)

const auto cellSize = p.s->font->cellSize;
const auto dottedLineType = horizontalShift ? ShadingType::DottedLineWide : ShadingType::DottedLine;
const auto dashedLineType = horizontalShift ? ShadingType::DashedLineWide : ShadingType::DashedLine;

const auto rowTop = static_cast<i16>(cellSize.y * y);
const auto rowBottom = static_cast<i16>(rowTop + cellSize.y);
Expand Down Expand Up @@ -1695,6 +1697,10 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y)
.size = { width, static_cast<u16>(rb - rt) },
.color = r.color,
};
if (shadingType == ShadingType::CurlyLine)
{
_getLastQuad().texcoord = _curlyLineTexCoord;
}
}
};

Expand Down Expand Up @@ -1724,10 +1730,18 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y)
{
appendHorizontalLine(r, p.s->font->underline, ShadingType::SolidLine);
}
if (r.lines.test(GridLines::HyperlinkUnderline))
if (r.lines.any(GridLines::DottedUnderline, GridLines::HyperlinkUnderline))
{
appendHorizontalLine(r, p.s->font->underline, dottedLineType);
}
if (r.lines.test(GridLines::DashedUnderline))
{
appendHorizontalLine(r, p.s->font->underline, dashedLineType);
}
if (r.lines.test(GridLines::CurlyUnderline))
{
appendHorizontalLine(r, p.s->font->curlyUnderline, ShadingType::CurlyLine);
}
if (r.lines.test(GridLines::DoubleUnderline))
{
for (const auto pos : p.s->font->doubleUnderline)
Expand All @@ -1742,6 +1756,91 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y)
}
}

void BackendD3D::_drawCurlylineToAtlas(const RenderingPayload& p)
{
const auto cellWidth = p.s->font->cellSize.x;

// draw cell-width worth of curlyline and reuse it as a tiling texture for
// longer lengths.
stbrp_rect rect{
.w = cellWidth,
.h = p.s->font->curlyUnderline.height,
};
if (!stbrp_pack_rects(&_rectPacker, &rect, 1))
{
_drawGlyphPrepareRetry(p);
return;
}

const auto baselineY = p.s->font->curlyUnderline.height / 2.0f;
const auto ctrlPointOffsetY = std::roundf(p.s->font->curlyUnderlineWaviness * p.s->font->fontSize);

wil::com_ptr<ID2D1PathGeometry> curlyLine;
THROW_IF_FAILED(p.d2dFactory->CreatePathGeometry(curlyLine.addressof()));

// The ends of the curve drawn by a PathGeometry is always titled at an
// angle of the tangent at the end points. Since we are going to tile them
// one after another, we need ends to be vertically flat. So we'll draw
// three curly lines, and cut the middle one off. This will give us a
// curly line with flat ends.
{
wil::com_ptr<ID2D1GeometrySink> sink;
curlyLine->Open(sink.addressof());

// first wave (starts at -cellSize.x)
sink->BeginFigure(
D2D1::Point2F(-cellWidth, baselineY),
D2D1_FIGURE_BEGIN_HOLLOW);

sink->AddBezier(
D2D1::BezierSegment(
D2D1::Point2F(-cellWidth / 2.0f, baselineY - ctrlPointOffsetY),
D2D1::Point2F(-cellWidth / 2.0f, baselineY + ctrlPointOffsetY),
D2D1::Point2F(0.0f, baselineY)));

// second wave (starts at 0.0f)
sink->AddBezier(
D2D1::BezierSegment(
D2D1::Point2F(cellWidth / 2.0f, baselineY - ctrlPointOffsetY),
D2D1::Point2F(cellWidth / 2.0f, baselineY + ctrlPointOffsetY),
D2D1::Point2F(cellWidth, baselineY)));

// third wave (starts at 'cellSize.x' px)
sink->AddBezier(
D2D1::BezierSegment(
D2D1::Point2F(cellWidth + cellWidth / 2.0f, baselineY - ctrlPointOffsetY),
D2D1::Point2F(cellWidth + cellWidth / 2.0f, baselineY + ctrlPointOffsetY),
D2D1::Point2F(2.0f * cellWidth, baselineY)));

sink->EndFigure(D2D1_FIGURE_END_OPEN);

sink->Close();
}

_d2dBeginDrawing();

// Clip everything but the middle wave
{
const auto clipRect = D2D1::RectF(
static_cast<f32>(rect.x),
static_cast<f32>(rect.y),
static_cast<f32>(rect.x + rect.w),
static_cast<f32>(rect.y + rect.h));
_d2dRenderTarget->PushAxisAlignedClip(clipRect, D2D1_ANTIALIAS_MODE_ALIASED);
}

auto const strokeWidth = p.s->font->underline.height;
_d2dRenderTarget->DrawGeometry(curlyLine.get(), _brush.get(), strokeWidth);

_d2dRenderTarget->PopAxisAlignedClip();

_d2dEndDrawing();

// store the texture coordinates
_curlyLineTexCoord.x = rect.x;
_curlyLineTexCoord.y = rect.y;
}

void BackendD3D::_drawCursorBackground(const RenderingPayload& p)
{
_cursorRects.clear();
Expand Down
11 changes: 8 additions & 3 deletions src/renderer/atlas/BackendD3D.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,14 @@ namespace Microsoft::Console::Render::Atlas
TextPassthrough = 3,
DottedLine = 4,
DottedLineWide = 5,
DashedLine = 6,
DashedLineWide = 7,
CurlyLine = 8,
// All items starting here will be drawing as a solid RGBA color
SolidLine = 6,
SolidLine = 9,

Cursor = 7,
Selection = 8,
Cursor = 10,
Selection = 11,

TextDrawingFirst = TextGrayscale,
TextDrawingLast = SolidLine,
Expand Down Expand Up @@ -224,6 +227,7 @@ namespace Microsoft::Console::Render::Atlas
void _drawGlyphPrepareRetry(const RenderingPayload& p);
void _splitDoubleHeightGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry);
void _drawGridlines(const RenderingPayload& p, u16 y);
void _drawCurlylineToAtlas(const RenderingPayload& p);
void _drawCursorBackground(const RenderingPayload& p);
ATLAS_ATTR_COLD void _drawCursorForeground();
ATLAS_ATTR_COLD size_t _drawCursorForegroundSlowPath(const CursorRect& c, size_t offset);
Expand Down Expand Up @@ -291,6 +295,7 @@ namespace Microsoft::Console::Render::Atlas
// The bounding rect of _cursorRects in pixels.
til::rect _cursorPosition;

u16x2 _curlyLineTexCoord;
bool _requiresContinuousRedraw = false;

#if ATLAS_DEBUG_SHOW_DIRTY
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/atlas/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -346,13 +346,15 @@ namespace Microsoft::Console::Render::Atlas
u16 baseline = 0;
u16 descender = 0;
u16 thinLineWidth = 0;
f32 curlyUnderlineWaviness = 0.28f; // Control points' height in `em` units.

FontDecorationPosition gridTop;
FontDecorationPosition gridBottom;
FontDecorationPosition gridLeft;
FontDecorationPosition gridRight;

FontDecorationPosition underline;
FontDecorationPosition curlyUnderline;
FontDecorationPosition strikethrough;
FontDecorationPosition doubleUnderline[2];
FontDecorationPosition overline;
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/atlas/shader_common.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
#define SHADING_TYPE_TEXT_PASSTHROUGH 3
#define SHADING_TYPE_DOTTED_LINE 4
#define SHADING_TYPE_DOTTED_LINE_WIDE 5
#define SHADING_TYPE_DASHED_LINE 6
#define SHADING_TYPE_DASHED_LINE_WIDE 7
#define SHADING_TYPE_CURLY_LINE 8
// clang-format on

struct VSData
Expand Down
32 changes: 32 additions & 0 deletions src/renderer/atlas/shader_ps.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,38 @@ Output main(PSData data) : SV_Target
weights = color.aaaa;
break;
}
case SHADING_TYPE_DASHED_LINE:
{
const bool on = frac(data.position.x / backgroundCellSize.x) < 0.5f;
color = on * premultiplyColor(data.color);
weights = color.aaaa;
break;
}
case SHADING_TYPE_DASHED_LINE_WIDE:
{
const bool on = frac(data.position.x / (2.0f * backgroundCellSize.x)) < 0.5f;
color = on * premultiplyColor(data.color);
weights = color.aaaa;
break;
}
case SHADING_TYPE_CURLY_LINE:
{
const float4 foreground = premultiplyColor(data.color);
const float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, data.color.rgb);
const float intensity = DWrite_CalcColorIntensity(data.color.rgb);

const float2 texcoord = {
data.texcoord.x % backgroundCellSize.x,
data.texcoord.y % backgroundCellSize.y
};
const float4 glyph = glyphAtlas[texcoord];
const float contrasted = DWrite_EnhanceContrast(glyph.a, blendEnhancedContrast);
const float alphaCorrected = DWrite_ApplyAlphaCorrection(contrasted, intensity, gammaRatios);

color = alphaCorrected * foreground;
weights = color.aaaa;
break;
}
default:
{
color = premultiplyColor(data.color);
Expand Down
41 changes: 41 additions & 0 deletions src/renderer/base/RenderSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,47 @@ std::pair<COLORREF, COLORREF> RenderSettings::GetAttributeColorsWithAlpha(const
return { fg, bg };
}

// Routine Description:
// - Calculates the RGB underline color of a given text attribute, using the
// current color table configuration and active render settings.
// - Returns the current foreground color when the underline color isn't set.
// Arguments:
// - attr - The TextAttribute to retrieve the underline color from.
// Return Value:
// - The color value of the attribute's underline.
COLORREF RenderSettings::GetAttributeUnderlineColor(const TextAttribute& attr) const noexcept
{
const auto [fg, bg] = GetAttributeColors(attr);
const auto ulTextColor = attr.GetUnderlineColor();
if (ulTextColor.IsDefault())
{
return fg;
}

const auto defaultUlIndex = GetColorAliasIndex(ColorAlias::DefaultForeground);
auto ul = ulTextColor.GetColor(_colorTable, defaultUlIndex, true);
if (attr.IsInvisible())
{
ul = bg;
}

// We intentionally aren't _only_ checking for attr.IsInvisible here, because we also want to
// catch the cases where the ul was intentionally set to be the same as the bg. In either case,
// don't adjust the underline color.
if constexpr (Feature_AdjustIndistinguishableText::IsEnabled())
{
if (
ul != bg &&
(_renderMode.test(Mode::AlwaysDistinguishableColors) ||
(_renderMode.test(Mode::IndexedDistinguishableColors) && ulTextColor.IsDefaultOrLegacy() && attr.GetBackground().IsDefaultOrLegacy())))
{
ul = ColorFix::GetPerceivableColor(ul, bg, 0.5f * 0.5f);
}
}

return ul;
}

// Routine Description:
// - Increments the position in the blink cycle, toggling the blink rendition
// state on every second call, potentially triggering a redraw of the given
Expand Down
14 changes: 11 additions & 3 deletions src/renderer/base/renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -955,13 +955,21 @@ GridLineSet Renderer::s_GetGridlines(const TextAttribute& textAttribute) noexcep
{
case UnderlineStyle::NoUnderline:
break;
case UnderlineStyle::SinglyUnderlined:
lines.set(GridLines::Underline);
break;
case UnderlineStyle::DoublyUnderlined:
lines.set(GridLines::DoubleUnderline);
break;
case UnderlineStyle::SinglyUnderlined:
case UnderlineStyle::CurlyUnderlined:
lines.set(GridLines::CurlyUnderline);
break;
case UnderlineStyle::DottedUnderlined:
lines.set(GridLines::DottedUnderline);
break;
case UnderlineStyle::DashedUnderlined:
lines.set(GridLines::DashedUnderline);
break;
default:
lines.set(GridLines::Underline);
break;
Expand Down Expand Up @@ -1002,8 +1010,8 @@ void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngin
// Return early if there are no lines to paint.
if (lines.any())
{
// Get the current foreground color to render the lines.
const auto rgb = _renderSettings.GetAttributeColors(textAttribute).first;
// Get the current underline color to render the lines.
const auto rgb = _renderSettings.GetAttributeUnderlineColor(textAttribute);
// Draw the lines
LOG_IF_FAILED(pEngine->PaintBufferGridLines(lines, rgb, cchLine, coordTarget));
}
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/inc/IRenderEngine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ namespace Microsoft::Console::Render
Right,
Underline,
DoubleUnderline,
CurlyUnderline,
DottedUnderline,
DashedUnderline,
Strikethrough,
HyperlinkUnderline
};
Expand Down
Loading