diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 3a565ee4812..f38eddb9d3f 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -344,6 +344,7 @@ CTRLVOLUME Ctxt CUF cupxy +curlyline CURRENTFONT currentmode CURRENTPAGE @@ -579,6 +580,7 @@ elems emacs EMPTYBOX enabledelayedexpansion +ENDCAP endptr endregion ENTIREBUFFER @@ -827,6 +829,7 @@ hostlib HPA hpcon HPCON +hpen hpj HPR HProvider @@ -1011,6 +1014,7 @@ LOBYTE localappdata locsrc Loewen +LOGBRUSH LOGFONT LOGFONTA LOGFONTW diff --git a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp index 4b456e7cd5b..d5baebadf26 100644 --- a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp @@ -57,7 +57,7 @@ namespace HRESULT InvalidateCircling(_Out_ bool* /*pForcePaint*/) noexcept { return S_OK; } HRESULT PaintBackground() noexcept { return S_OK; } HRESULT PaintBufferLine(std::span /*clusters*/, til::point /*coord*/, bool /*fTrimLeft*/, bool /*lineWrapped*/) noexcept { return S_OK; } - HRESULT PaintBufferGridLines(GridLineSet /*lines*/, COLORREF /*color*/, size_t /*cchLine*/, til::point /*coordTarget*/) noexcept { return S_OK; } + HRESULT PaintBufferGridLines(GridLineSet /*lines*/, COLORREF /*gridlineColor*/, COLORREF /*underlineColor*/, size_t /*cchLine*/, til::point /*coordTarget*/) noexcept { return S_OK; } HRESULT PaintSelection(const til::rect& /*rect*/) noexcept { return S_OK; } HRESULT PaintCursor(const CursorOptions& /*options*/) noexcept { return S_OK; } HRESULT UpdateDrawingBrushes(const TextAttribute& /*textAttributes*/, const RenderSettings& /*renderSettings*/, gsl::not_null /*pData*/, bool /*usingSoftFont*/, bool /*isSettingDefaultBrushes*/) noexcept { return S_OK; } diff --git a/src/interactivity/onecore/BgfxEngine.cpp b/src/interactivity/onecore/BgfxEngine.cpp index ce6b903ffd1..d508603d5f9 100644 --- a/src/interactivity/onecore/BgfxEngine.cpp +++ b/src/interactivity/onecore/BgfxEngine.cpp @@ -147,9 +147,10 @@ CATCH_RETURN() CATCH_RETURN() } -[[nodiscard]] HRESULT BgfxEngine::PaintBufferGridLines(GridLineSet const /*lines*/, - COLORREF const /*color*/, - size_t const /*cchLine*/, +[[nodiscard]] HRESULT BgfxEngine::PaintBufferGridLines(const GridLineSet /*lines*/, + const COLORREF /*gridlineColor*/, + const COLORREF /*underlineColor*/, + const size_t /*cchLine*/, const til::point /*coordTarget*/) noexcept { return S_OK; diff --git a/src/interactivity/onecore/BgfxEngine.hpp b/src/interactivity/onecore/BgfxEngine.hpp index 31fa49c8522..e9759ce0306 100644 --- a/src/interactivity/onecore/BgfxEngine.hpp +++ b/src/interactivity/onecore/BgfxEngine.hpp @@ -51,7 +51,7 @@ namespace Microsoft::Console::Render const til::point coord, const bool trimLeft, const bool lineWrapped) noexcept override; - [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet const lines, COLORREF const color, size_t const cchLine, til::point const coordTarget) noexcept override; + [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index e46bb250835..7438b93543e 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -375,7 +375,7 @@ try } CATCH_RETURN() -[[nodiscard]] HRESULT AtlasEngine::PaintBufferGridLines(const GridLineSet lines, const COLORREF color, const size_t cchLine, const til::point coordTarget) noexcept +[[nodiscard]] HRESULT AtlasEngine::PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept try { const auto shift = gsl::narrow_cast(_api.lineRendition != LineRendition::SingleWidth); @@ -383,8 +383,9 @@ try const auto y = gsl::narrow_cast(clamp(coordTarget.y, 0, _p.s->viewportCellCount.y)); const auto from = gsl::narrow_cast(clamp(x << shift, 0, _p.s->viewportCellCount.x - 1)); const auto to = gsl::narrow_cast(clamp((x + cchLine) << shift, from, _p.s->viewportCellCount.x)); - const auto fg = gsl::narrow_cast(color) | 0xff000000; - _p.rows[y]->gridLineRanges.emplace_back(lines, fg, from, to); + const auto glColor = gsl::narrow_cast(gridlineColor) | 0xff000000; + const auto ulColor = gsl::narrow_cast(underlineColor) | 0xff000000; + _p.rows[y]->gridLineRanges.emplace_back(lines, glColor, ulColor, from, to); return S_OK; } CATCH_RETURN() diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h index b135a8989f8..4ec09a9e931 100644 --- a/src/renderer/atlas/AtlasEngine.h +++ b/src/renderer/atlas/AtlasEngine.h @@ -43,7 +43,7 @@ namespace Microsoft::Console::Render::Atlas [[nodiscard]] HRESULT PrepareLineTransform(LineRendition lineRendition, til::CoordType targetRow, til::CoordType viewportLeft) noexcept override; [[nodiscard]] HRESULT PaintBackground() noexcept override; [[nodiscard]] HRESULT PaintBufferLine(std::span clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept override; - [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF color, size_t cchLine, til::point coordTarget) noexcept override; + [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, gsl::not_null pData, bool usingSoftFont, bool isSettingDefaultBrushes) noexcept override; diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp index 301738954aa..ea4ea0923eb 100644 --- a/src/renderer/atlas/BackendD2D.cpp +++ b/src/renderer/atlas/BackendD2D.cpp @@ -409,7 +409,7 @@ void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* ro D2D1_POINT_2F point0{ 0, static_cast(textCellCenter) }; D2D1_POINT_2F point1{ 0, static_cast(textCellCenter + cellSize.y) }; - const auto brush = _brushWithColor(r.color); + const auto brush = _brushWithColor(r.gridlineColor); const f32 w = pos.height; const f32 hw = w * 0.5f; @@ -421,11 +421,11 @@ void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* ro _renderTarget->DrawLine(point0, point1, brush, w, nullptr); } }; - const auto appendHorizontalLine = [&](const GridLineRange& r, FontDecorationPosition pos, ID2D1StrokeStyle* strokeStyle) { + const auto appendHorizontalLine = [&](const GridLineRange& r, FontDecorationPosition pos, ID2D1StrokeStyle* strokeStyle, const u32 color) { const auto from = r.from >> widthShift; const auto to = r.to >> widthShift; - const auto brush = _brushWithColor(r.color); + const auto brush = _brushWithColor(color); const f32 w = pos.height; const f32 centerY = textCellCenter + pos.position + w * 0.5f; const D2D1_POINT_2F point0{ static_cast(from * cellSize.x), centerY }; @@ -448,32 +448,32 @@ void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* ro } if (r.lines.test(GridLines::Top)) { - appendHorizontalLine(r, p.s->font->gridTop, nullptr); + appendHorizontalLine(r, p.s->font->gridTop, nullptr, r.gridlineColor); } if (r.lines.test(GridLines::Bottom)) { - appendHorizontalLine(r, p.s->font->gridBottom, nullptr); + appendHorizontalLine(r, p.s->font->gridBottom, nullptr, r.gridlineColor); + } + if (r.lines.test(GridLines::Strikethrough)) + { + appendHorizontalLine(r, p.s->font->strikethrough, nullptr, r.gridlineColor); } if (r.lines.test(GridLines::Underline)) { - appendHorizontalLine(r, p.s->font->underline, nullptr); + appendHorizontalLine(r, p.s->font->underline, nullptr, r.underlineColor); } - if (r.lines.test(GridLines::HyperlinkUnderline)) + else if (r.lines.any(GridLines::DottedUnderline, GridLines::HyperlinkUnderline)) { - appendHorizontalLine(r, p.s->font->underline, _dottedStrokeStyle.get()); + appendHorizontalLine(r, p.s->font->underline, _dottedStrokeStyle.get(), r.underlineColor); } - if (r.lines.test(GridLines::DoubleUnderline)) + else if (r.lines.test(GridLines::DoubleUnderline)) { for (const auto pos : p.s->font->doubleUnderline) { - appendHorizontalLine(r, pos, nullptr); + appendHorizontalLine(r, pos, nullptr, r.underlineColor); } } - if (r.lines.test(GridLines::Strikethrough)) - { - appendHorizontalLine(r, p.s->font->strikethrough, nullptr); - } } } diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index 19334b3a9d4..41fefa6ea2b 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -90,7 +90,8 @@ BackendD3D::BackendD3D(const RenderingPayload& p) { static constexpr D3D11_INPUT_ELEMENT_DESC layout[]{ { "SV_Position", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, - { "shadingType", 0, DXGI_FORMAT_R32_UINT, 1, offsetof(QuadInstance, shadingType), D3D11_INPUT_PER_INSTANCE_DATA, 1 }, + { "shadingType", 0, DXGI_FORMAT_R16_UINT, 1, offsetof(QuadInstance, shadingType), D3D11_INPUT_PER_INSTANCE_DATA, 1 }, + { "renditionScale", 0, DXGI_FORMAT_R8G8_UINT, 1, offsetof(QuadInstance, renditionScale), D3D11_INPUT_PER_INSTANCE_DATA, 1 }, { "position", 0, DXGI_FORMAT_R16G16_SINT, 1, offsetof(QuadInstance, position), D3D11_INPUT_PER_INSTANCE_DATA, 1 }, { "size", 0, DXGI_FORMAT_R16G16_UINT, 1, offsetof(QuadInstance, size), D3D11_INPUT_PER_INSTANCE_DATA, 1 }, { "texcoord", 0, DXGI_FORMAT_R16G16_UINT, 1, offsetof(QuadInstance, texcoord), D3D11_INPUT_PER_INSTANCE_DATA, 1 }, @@ -305,6 +306,37 @@ void BackendD3D::_updateFontDependents(const RenderingPayload& p) { const auto& font = *p.s->font; + // The max height of Curly line peak in `em` units. + const auto maxCurlyLinePeakHeightEm = 0.075f; + // We aim for atleast 1px height, but since we draw 1px smaller curly line, + // we aim for 2px height as a result. + const auto minCurlyLinePeakHeight = 2.0f; + + // Curlyline uses the gap between cell bottom and singly underline position + // as the height of the wave's peak. The baseline for curly-line is at the + // middle of singly underline. The gap could be too big, so we also apply + // a limit on the peak height. + const auto strokeHalfWidth = font.underline.height / 2.0f; + const auto underlineMidY = font.underline.position + strokeHalfWidth; + const auto cellBottomGap = font.cellSize.y - underlineMidY - strokeHalfWidth; + const auto maxCurlyLinePeakHeight = maxCurlyLinePeakHeightEm * font.fontSize; + auto curlyLinePeakHeight = std::min(cellBottomGap, maxCurlyLinePeakHeight); + + // When it's too small to be curly, make it straight. + if (curlyLinePeakHeight < minCurlyLinePeakHeight) + { + curlyLinePeakHeight = 0; + } + + // We draw a smaller curly line (-1px) to avoid clipping due to the rounding. + _curlyLineDrawPeakHeight = std::max(0.0f, curlyLinePeakHeight - 1.0f); + + const auto curlyUnderlinePos = font.underline.position - curlyLinePeakHeight; + const auto curlyUnderlineWidth = 2.0f * (curlyLinePeakHeight + strokeHalfWidth); + const auto curlyUnderlinePosU16 = gsl::narrow_cast(lrintf(curlyUnderlinePos)); + const auto curlyUnderlineWidthU16 = gsl::narrow_cast(lrintf(curlyUnderlineWidth)); + _curlyUnderline = { curlyUnderlinePosU16, curlyUnderlineWidthU16 }; + DWrite_GetRenderParams(p.dwriteFactory.get(), &_gamma, &_cleartypeEnhancedContrast, &_grayscaleEnhancedContrast, _textRenderingParams.put()); // Clearing the atlas requires BeginDraw(), which is expensive. Defer this until we need Direct2D anyways. _fontChangedResetGlyphAtlas = true; @@ -543,6 +575,9 @@ void BackendD3D::_recreateConstBuffer(const RenderingPayload& p) const DWrite_GetGammaRatios(_gamma, data.gammaRatios); data.enhancedContrast = p.s->font->antialiasingMode == AntialiasingMode::ClearType ? _cleartypeEnhancedContrast : _grayscaleEnhancedContrast; data.underlineWidth = p.s->font->underline.height; + data.curlyLineWaveFreq = 2.0f * 3.14f / p.s->font->cellSize.x; + data.curlyLinePeakHeight = _curlyLineDrawPeakHeight; + data.curlyLineCellOffset = p.s->font->underline.position + p.s->font->underline.height / 2.0f; p.deviceContext->UpdateSubresource(_psConstantBuffer.get(), 0, nullptr, &data, 0, 0); } } @@ -1024,7 +1059,7 @@ void BackendD3D::_drawText(RenderingPayload& p) goto drawGlyphRetry; } - if (glyphEntry.data.GetShadingType() != ShadingType::Default) + if (glyphEntry.data.shadingType != ShadingType::Default) { auto l = static_cast(lrintf((baselineX + row->glyphOffsets[x].advanceOffset) * scaleX)); auto t = static_cast(lrintf((baselineY - row->glyphOffsets[x].ascenderOffset) * scaleY)); @@ -1036,7 +1071,7 @@ void BackendD3D::_drawText(RenderingPayload& p) row->dirtyBottom = std::max(row->dirtyBottom, t + glyphEntry.data.size.y); _appendQuad() = { - .shadingType = glyphEntry.data.GetShadingType(), + .shadingType = glyphEntry.data.shadingType, .position = { static_cast(l), static_cast(t) }, .size = glyphEntry.data.size, .texcoord = glyphEntry.data.texcoord, @@ -1458,7 +1493,7 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryI const auto triggerRight = _ligatureOverhangTriggerRight * horizontalScale; const auto overlapSplit = rect.w >= p.s->font->cellSize.x && (bl <= triggerLeft || br >= triggerRight); - glyphEntry.data.shadingType = static_cast(isColorGlyph ? ShadingType::TextPassthrough : _textShadingType); + glyphEntry.data.shadingType = isColorGlyph ? ShadingType::TextPassthrough : _textShadingType; glyphEntry.data.overlapSplit = overlapSplit; glyphEntry.data.offset.x = bl; glyphEntry.data.offset.y = bt; @@ -1527,7 +1562,7 @@ bool BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFa _drawSoftFontGlyphInBitmap(p, glyphEntry); _d2dRenderTarget->DrawBitmap(_softFontBitmap.get(), &dest, 1, interpolation, nullptr, nullptr); - glyphEntry.data.shadingType = static_cast(ShadingType::TextGrayscale); + glyphEntry.data.shadingType = ShadingType::TextGrayscale; glyphEntry.data.overlapSplit = 0; glyphEntry.data.offset.x = 0; glyphEntry.data.offset.y = -baseline; @@ -1631,11 +1666,11 @@ void BackendD3D::_splitDoubleHeightGlyph(const RenderingPayload& p, const AtlasF // double-height row. This effectively turns the other (unneeded) side into whitespace. if (!top.data.size.y) { - top.data.shadingType = static_cast(ShadingType::Default); + top.data.shadingType = ShadingType::Default; } if (!bottom.data.size.y) { - bottom.data.shadingType = static_cast(ShadingType::Default); + bottom.data.shadingType = ShadingType::Default; } } @@ -1647,8 +1682,6 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y) const auto verticalShift = static_cast(row->lineRendition >= LineRendition::DoubleHeightTop); const auto cellSize = p.s->font->cellSize; - const auto dottedLineType = horizontalShift ? ShadingType::DottedLineWide : ShadingType::DottedLine; - const auto rowTop = static_cast(cellSize.y * y); const auto rowBottom = static_cast(rowTop + cellSize.y); @@ -1675,11 +1708,11 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y) .shadingType = ShadingType::SolidLine, .position = { static_cast(posX), rowTop }, .size = { width, p.s->font->cellSize.y }, - .color = r.color, + .color = r.gridlineColor, }; } }; - const auto appendHorizontalLine = [&](const GridLineRange& r, FontDecorationPosition pos, ShadingType shadingType) { + const auto appendHorizontalLine = [&](const GridLineRange& r, FontDecorationPosition pos, ShadingType shadingType, const u32 color) { const auto offset = pos.position << verticalShift; const auto height = static_cast(pos.height << verticalShift); @@ -1695,9 +1728,10 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y) { _appendQuad() = { .shadingType = shadingType, + .renditionScale = { static_cast(1 << horizontalShift), static_cast(1 << verticalShift) }, .position = { left, static_cast(rt) }, .size = { width, static_cast(rb - rt) }, - .color = r.color, + .color = color, }; } }; @@ -1717,32 +1751,40 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y) } if (r.lines.test(GridLines::Top)) { - appendHorizontalLine(r, p.s->font->gridTop, ShadingType::SolidLine); + appendHorizontalLine(r, p.s->font->gridTop, ShadingType::SolidLine, r.gridlineColor); } if (r.lines.test(GridLines::Bottom)) { - appendHorizontalLine(r, p.s->font->gridBottom, ShadingType::SolidLine); + appendHorizontalLine(r, p.s->font->gridBottom, ShadingType::SolidLine, r.gridlineColor); + } + if (r.lines.test(GridLines::Strikethrough)) + { + appendHorizontalLine(r, p.s->font->strikethrough, ShadingType::SolidLine, r.gridlineColor); } if (r.lines.test(GridLines::Underline)) { - appendHorizontalLine(r, p.s->font->underline, ShadingType::SolidLine); + appendHorizontalLine(r, p.s->font->underline, ShadingType::SolidLine, r.underlineColor); } - if (r.lines.test(GridLines::HyperlinkUnderline)) + else if (r.lines.any(GridLines::DottedUnderline, GridLines::HyperlinkUnderline)) { - appendHorizontalLine(r, p.s->font->underline, dottedLineType); + appendHorizontalLine(r, p.s->font->underline, ShadingType::DottedLine, r.underlineColor); } - if (r.lines.test(GridLines::DoubleUnderline)) + else if (r.lines.test(GridLines::DashedUnderline)) + { + appendHorizontalLine(r, p.s->font->underline, ShadingType::DashedLine, r.underlineColor); + } + else if (r.lines.test(GridLines::CurlyUnderline)) + { + appendHorizontalLine(r, _curlyUnderline, ShadingType::CurlyLine, r.underlineColor); + } + else if (r.lines.test(GridLines::DoubleUnderline)) { for (const auto pos : p.s->font->doubleUnderline) { - appendHorizontalLine(r, pos, ShadingType::SolidLine); + appendHorizontalLine(r, pos, ShadingType::SolidLine, r.underlineColor); } } - if (r.lines.test(GridLines::Strikethrough)) - { - appendHorizontalLine(r, p.s->font->strikethrough, ShadingType::SolidLine); - } } } @@ -2042,6 +2084,8 @@ size_t BackendD3D::_drawCursorForegroundSlowPath(const CursorRect& c, size_t off auto& target = _instances[offset + i]; target.shadingType = it.shadingType; + target.renditionScale.x = it.renditionScale.x; + target.renditionScale.y = it.renditionScale.y; target.position.x = static_cast(cutout.left); target.position.y = static_cast(cutout.top); target.size.x = static_cast(cutout.right - cutout.left); @@ -2059,6 +2103,8 @@ size_t BackendD3D::_drawCursorForegroundSlowPath(const CursorRect& c, size_t off auto& target = cutoutCount ? _appendQuad() : _instances[offset]; target.shadingType = it.shadingType; + target.renditionScale.x = it.renditionScale.x; + target.renditionScale.y = it.renditionScale.y; target.position.x = static_cast(intersectionL); target.position.y = static_cast(intersectionT); target.size.x = static_cast(intersectionR - intersectionL); diff --git a/src/renderer/atlas/BackendD3D.h b/src/renderer/atlas/BackendD3D.h index 14f4ca2943e..0befe8fe342 100644 --- a/src/renderer/atlas/BackendD3D.h +++ b/src/renderer/atlas/BackendD3D.h @@ -42,6 +42,9 @@ namespace Microsoft::Console::Render::Atlas alignas(sizeof(f32x4)) f32 gammaRatios[4]{}; alignas(sizeof(f32)) f32 enhancedContrast = 0; alignas(sizeof(f32)) f32 underlineWidth = 0; + alignas(sizeof(f32)) f32 curlyLinePeakHeight = 0; + alignas(sizeof(f32)) f32 curlyLineWaveFreq = 0; + alignas(sizeof(f32)) f32 curlyLineCellOffset = 0; #pragma warning(suppress : 4324) // 'PSConstBuffer': structure was padded due to alignment specifier }; @@ -55,7 +58,7 @@ namespace Microsoft::Console::Render::Atlas #pragma warning(suppress : 4324) // 'CustomConstBuffer': structure was padded due to alignment specifier }; - enum class ShadingType : u32 + enum class ShadingType : u16 { Default = 0, Background = 0, @@ -66,12 +69,13 @@ namespace Microsoft::Console::Render::Atlas TextClearType = 2, TextPassthrough = 3, DottedLine = 4, - DottedLineWide = 5, + DashedLine = 5, + CurlyLine = 6, // All items starting here will be drawing as a solid RGBA color - SolidLine = 6, + SolidLine = 7, - Cursor = 7, - Selection = 8, + Cursor = 8, + Selection = 9, TextDrawingFirst = TextGrayscale, TextDrawingLast = SolidLine, @@ -86,7 +90,8 @@ namespace Microsoft::Console::Render::Atlas // impact on performance and power draw. If (when?) displays with >32k resolution make their // appearance in the future, this should be changed to f32x2. But if you do so, please change // all other occurrences of i16x2 positions/offsets throughout the class to keep it consistent. - alignas(u32) ShadingType shadingType; + alignas(u16) ShadingType shadingType; + alignas(u16) u8x2 renditionScale; alignas(u32) i16x2 position; alignas(u32) u16x2 size; alignas(u32) u16x2 texcoord; @@ -95,16 +100,11 @@ namespace Microsoft::Console::Render::Atlas struct alignas(u32) AtlasGlyphEntryData { - u16 shadingType; + ShadingType shadingType; u16 overlapSplit; i16x2 offset; u16x2 size; u16x2 texcoord; - - constexpr ShadingType GetShadingType() const noexcept - { - return static_cast(shadingType); - } }; // NOTE: Don't initialize any members in this struct. This ensures that no @@ -291,6 +291,9 @@ namespace Microsoft::Console::Render::Atlas // The bounding rect of _cursorRects in pixels. til::rect _cursorPosition; + f32 _curlyLineDrawPeakHeight = 0; + FontDecorationPosition _curlyUnderline; + bool _requiresContinuousRedraw = false; #if ATLAS_DEBUG_SHOW_DIRTY diff --git a/src/renderer/atlas/common.h b/src/renderer/atlas/common.h index b8fa3ac0d59..014f5487118 100644 --- a/src/renderer/atlas/common.h +++ b/src/renderer/atlas/common.h @@ -125,6 +125,7 @@ namespace Microsoft::Console::Render::Atlas }; using u8 = uint8_t; + using u8x2 = vec2; using u16 = uint16_t; using u16x2 = vec2; @@ -426,7 +427,8 @@ namespace Microsoft::Console::Render::Atlas struct GridLineRange { GridLineSet lines; - u32 color = 0; + u32 gridlineColor = 0; + u32 underlineColor = 0; u16 from = 0; u16 to = 0; }; diff --git a/src/renderer/atlas/shader_common.hlsl b/src/renderer/atlas/shader_common.hlsl index 957b17ac6ad..d712f081f28 100644 --- a/src/renderer/atlas/shader_common.hlsl +++ b/src/renderer/atlas/shader_common.hlsl @@ -7,13 +7,15 @@ #define SHADING_TYPE_TEXT_CLEARTYPE 2 #define SHADING_TYPE_TEXT_PASSTHROUGH 3 #define SHADING_TYPE_DOTTED_LINE 4 -#define SHADING_TYPE_DOTTED_LINE_WIDE 5 +#define SHADING_TYPE_DASHED_LINE 5 +#define SHADING_TYPE_CURLY_LINE 6 // clang-format on struct VSData { float2 vertex : SV_Position; uint shadingType : shadingType; + uint2 renditionScale : renditionScale; int2 position : position; uint2 size : size; uint2 texcoord : texcoord; @@ -25,6 +27,7 @@ struct PSData float4 position : SV_Position; float2 texcoord : texcoord; nointerpolation uint shadingType : shadingType; + nointerpolation uint2 renditionScale : renditionScale; nointerpolation float4 color : color; }; diff --git a/src/renderer/atlas/shader_ps.hlsl b/src/renderer/atlas/shader_ps.hlsl index 2cb92947ce2..e19ba955fe5 100644 --- a/src/renderer/atlas/shader_ps.hlsl +++ b/src/renderer/atlas/shader_ps.hlsl @@ -12,6 +12,9 @@ cbuffer ConstBuffer : register(b0) float4 gammaRatios; float enhancedContrast; float underlineWidth; + float curlyLinePeakHeight; + float curlyLineWaveFreq; + float curlyLineCellOffset; } Texture2D background : register(t0); @@ -73,18 +76,36 @@ Output main(PSData data) : SV_Target } case SHADING_TYPE_DOTTED_LINE: { - const bool on = frac(data.position.x / (2.0f * underlineWidth)) < 0.5f; + const bool on = frac(data.position.x / (2.0f * underlineWidth * data.renditionScale.x)) < 0.5f; color = on * premultiplyColor(data.color); weights = color.aaaa; break; } - case SHADING_TYPE_DOTTED_LINE_WIDE: + case SHADING_TYPE_DASHED_LINE: { - const bool on = frac(data.position.x / (4.0f * underlineWidth)) < 0.5f; + const bool on = frac(data.position.x / (backgroundCellSize.x * data.renditionScale.x)) < 0.5f; color = on * premultiplyColor(data.color); weights = color.aaaa; break; } + case SHADING_TYPE_CURLY_LINE: + { + uint cellRow = floor(data.position.y / backgroundCellSize.y); + // Use the previous cell when drawing 'Double Height' curly line. + cellRow -= data.renditionScale.y - 1; + const float cellTop = cellRow * backgroundCellSize.y; + const float centerY = cellTop + curlyLineCellOffset * data.renditionScale.y; + const float strokeWidthHalf = underlineWidth * data.renditionScale.y / 2.0f; + const float amp = curlyLinePeakHeight * data.renditionScale.y; + const float freq = curlyLineWaveFreq / data.renditionScale.x; + + const float s = sin(data.position.x * freq); + const float d = abs(centerY - (s * amp) - data.position.y); + const float a = 1 - saturate(d - strokeWidthHalf); + color = a * premultiplyColor(data.color); + weights = color.aaaa; + break; + } default: { color = premultiplyColor(data.color); diff --git a/src/renderer/atlas/shader_vs.hlsl b/src/renderer/atlas/shader_vs.hlsl index 49b9030b156..eb96fcf0e45 100644 --- a/src/renderer/atlas/shader_vs.hlsl +++ b/src/renderer/atlas/shader_vs.hlsl @@ -15,6 +15,7 @@ PSData main(VSData data) PSData output; output.color = data.color; output.shadingType = data.shadingType; + output.renditionScale = data.renditionScale; // positionScale is expected to be float2(2.0f / sizeInPixel.x, -2.0f / sizeInPixel.y). Together with the // addition below this will transform our "position" from pixel into normalized device coordinate (NDC) space. output.position.xy = (data.position + data.vertex.xy * data.size) * positionScale + float2(-1.0f, 1.0f); diff --git a/src/renderer/base/RenderSettings.cpp b/src/renderer/base/RenderSettings.cpp index 03c4795635a..f885957796a 100644 --- a/src/renderer/base/RenderSettings.cpp +++ b/src/renderer/base/RenderSettings.cpp @@ -212,6 +212,47 @@ std::pair 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 diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index 917c5ba05f9..2b328ee07f9 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -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; @@ -1002,10 +1010,11 @@ 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 foreground and underline colors to render the lines. + const auto fg = _renderSettings.GetAttributeColors(textAttribute).first; + const auto underlineColor = _renderSettings.GetAttributeUnderlineColor(textAttribute); // Draw the lines - LOG_IF_FAILED(pEngine->PaintBufferGridLines(lines, rgb, cchLine, coordTarget)); + LOG_IF_FAILED(pEngine->PaintBufferGridLines(lines, fg, underlineColor, cchLine, coordTarget)); } } diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index f74bf528dc1..064d219f72a 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -1696,14 +1696,16 @@ CATCH_RETURN() // - Paints lines around cells (draws in pieces of the grid) // Arguments: // - lines - Which grid lines (top, left, bottom, right) to draw -// - color - The color to use for drawing the lines +// - gridlineColor - The color to use for drawing the gridlines +// - underlineColor - The color to use for drawing the underlines // - cchLine - Length of the line to draw in character cells // - coordTarget - The X,Y character position in the grid where we should start drawing // - We will draw rightward (+X) from here // Return Value: // - S_OK or relevant DirectX error [[nodiscard]] HRESULT DxEngine::PaintBufferGridLines(const GridLineSet lines, - COLORREF const color, + const COLORREF gridlineColor, + const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept try @@ -1711,8 +1713,6 @@ try const auto existingColor = _d2dBrushForeground->GetColor(); const auto restoreBrushOnExit = wil::scope_exit([&]() noexcept { _d2dBrushForeground->SetColor(existingColor); }); - _d2dBrushForeground->SetColor(_ColorFFromColorRef(color | 0xff000000)); - const auto font = _fontRenderData->GlyphCell().to_d2d_size(); const D2D_POINT_2F target = { coordTarget.x * font.width, coordTarget.y * font.height }; const auto fullRunWidth = font.width * gsl::narrow_cast(cchLine); @@ -1721,10 +1721,12 @@ try _d2dDeviceContext->DrawLine({ x0, y0 }, { x1, y1 }, _d2dBrushForeground.Get(), strokeWidth, _strokeStyle.Get()); }; - const auto DrawHyperlinkLine = [=](const auto x0, const auto y0, const auto x1, const auto y1, const auto strokeWidth) noexcept { + const auto DrawDottedLine = [=](const auto x0, const auto y0, const auto x1, const auto y1, const auto strokeWidth) noexcept { _d2dDeviceContext->DrawLine({ x0, y0 }, { x1, y1 }, _d2dBrushForeground.Get(), strokeWidth, _dashStrokeStyle.Get()); }; + _d2dBrushForeground->SetColor(_ColorFFromColorRef(gridlineColor | 0xff000000)); + // NOTE: Line coordinates are centered within the line, so they need to be // offset by half the stroke width. For the start coordinate we add half // the stroke width, and for the end coordinate we subtract half the width. @@ -1773,10 +1775,22 @@ try } } + if (lines.test(GridLines::Strikethrough)) + { + const auto halfStrikethroughWidth = lineMetrics.strikethroughWidth / 2.0f; + const auto startX = target.x + halfStrikethroughWidth; + const auto endX = target.x + fullRunWidth - halfStrikethroughWidth; + const auto y = target.y + lineMetrics.strikethroughOffset; + + DrawLine(startX, y, endX, y, lineMetrics.strikethroughWidth); + } + + _d2dBrushForeground->SetColor(_ColorFFromColorRef(underlineColor | 0xff000000)); + // In the case of the underline and strikethrough offsets, the stroke width // is already accounted for, so they don't require further adjustments. - if (lines.any(GridLines::Underline, GridLines::DoubleUnderline, GridLines::HyperlinkUnderline)) + if (lines.any(GridLines::Underline, GridLines::DoubleUnderline, GridLines::DottedUnderline, GridLines::HyperlinkUnderline)) { const auto halfUnderlineWidth = lineMetrics.underlineWidth / 2.0f; const auto startX = target.x + halfUnderlineWidth; @@ -1788,9 +1802,9 @@ try DrawLine(startX, y, endX, y, lineMetrics.underlineWidth); } - if (lines.test(GridLines::HyperlinkUnderline)) + if (lines.any(GridLines::DottedUnderline, GridLines::HyperlinkUnderline)) { - DrawHyperlinkLine(startX, y, endX, y, lineMetrics.underlineWidth); + DrawDottedLine(startX, y, endX, y, lineMetrics.underlineWidth); } if (lines.test(GridLines::DoubleUnderline)) @@ -1801,16 +1815,6 @@ try } } - if (lines.test(GridLines::Strikethrough)) - { - const auto halfStrikethroughWidth = lineMetrics.strikethroughWidth / 2.0f; - const auto startX = target.x + halfStrikethroughWidth; - const auto endX = target.x + fullRunWidth - halfStrikethroughWidth; - const auto y = target.y + lineMetrics.strikethroughOffset; - - DrawLine(startX, y, endX, y, lineMetrics.strikethroughWidth); - } - return S_OK; } CATCH_RETURN() diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index bfb11205a05..877b3f1adfa 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -106,7 +106,7 @@ namespace Microsoft::Console::Render const bool fTrimLeft, const bool lineWrapped) noexcept override; - [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet const lines, COLORREF const color, size_t const cchLine, til::point const coordTarget) noexcept override; + [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp index a1ab2290a97..2cc5b4dc14c 100644 --- a/src/renderer/gdi/gdirenderer.hpp +++ b/src/renderer/gdi/gdirenderer.hpp @@ -52,7 +52,8 @@ namespace Microsoft::Console::Render const bool trimLeft, const bool lineWrapped) noexcept override; [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, - const COLORREF color, + const COLORREF gridlineColor, + const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; @@ -120,6 +121,7 @@ namespace Microsoft::Console::Render int underlineWidth; int strikethroughOffset; int strikethroughWidth; + int curlylinePeakHeight; }; LineMetrics _lineMetrics; diff --git a/src/renderer/gdi/paint.cpp b/src/renderer/gdi/paint.cpp index 17dc9bddd82..9722703d105 100644 --- a/src/renderer/gdi/paint.cpp +++ b/src/renderer/gdi/paint.cpp @@ -509,27 +509,24 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) // - Draws up to one line worth of grid lines on top of characters. // Arguments: // - lines - Enum defining which edges of the rectangle to draw -// - color - The color to use for drawing the edges. +// - gridlineColor - The color to use for drawing the gridlines. +// - underlineColor - The color to use for drawing the underlines. // - cchLine - How many characters we should draw the grid lines along (left to right in a row) // - coordTarget - The starting X/Y position of the first character to draw on. // 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 color, const size_t cchLine, const til::point coordTarget) noexcept +[[nodiscard]] HRESULT GdiEngine::PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept { LOG_IF_FAILED(_FlushBufferLines()); // Convert the target from characters to pixels. const auto ptTarget = coordTarget * _GetFontSize(); - // Set the brush color as requested and save the previous brush to restore at the end. - wil::unique_hbrush hbr(CreateSolidBrush(color)); - RETURN_HR_IF_NULL(E_FAIL, hbr.get()); - - wil::unique_hbrush hbrPrev(SelectBrush(_hdcMemoryContext, hbr.get())); - RETURN_HR_IF_NULL(E_FAIL, hbrPrev.get()); - hbr.release(); // If SelectBrush was successful, GDI owns the brush. Release for now. - // On exit, be sure we try to put the brush back how it was originally. - auto restoreBrushOnExit = wil::scope_exit([&] { hbr.reset(SelectBrush(_hdcMemoryContext, hbrPrev.get())); }); + // Create a brush with the gridline color, and apply it. + wil::unique_hbrush hbr(CreateSolidBrush(gridlineColor)); + RETURN_HR_IF_NULL(E_FAIL, hbr.get()); + const auto prevBrush = wil::SelectObject(_hdcMemoryContext, hbr.get()); + RETURN_HR_IF_NULL(E_FAIL, prevBrush.get()); // Get the font size so we know the size of the rectangle lines we'll be inscribing. const auto fontWidth = _GetFontSize().width; @@ -539,6 +536,31 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) const auto DrawLine = [=](const auto x, const auto y, const auto w, const auto h) { return PatBlt(_hdcMemoryContext, x, y, w, h, PATCOPY); }; + const auto DrawStrokedLine = [&](const auto x, const auto y, const auto w) { + RETURN_HR_IF(E_FAIL, !MoveToEx(_hdcMemoryContext, x, y, nullptr)); + RETURN_HR_IF(E_FAIL, !LineTo(_hdcMemoryContext, x + w, y)); + return S_OK; + }; + const auto DrawCurlyLine = [&](const auto x, const auto y, const auto cCurlyLines) { + const auto curlyLineWidth = fontWidth; + const auto curlyLineHalfWidth = lrintf(curlyLineWidth / 2.0f); + const auto controlPointHeight = gsl::narrow_cast(std::floor(3.5f * _lineMetrics.curlylinePeakHeight)); + // Each curlyLine requires 3 `POINT`s + const auto cPoints = gsl::narrow(3 * cCurlyLines); + std::vector points; + points.reserve(cPoints); + auto start = x; + for (auto i = 0u; i < cCurlyLines; i++) + { + points.emplace_back(start + curlyLineHalfWidth, y - controlPointHeight); + points.emplace_back(start + curlyLineHalfWidth, y + controlPointHeight); + points.emplace_back(start + curlyLineWidth, y); + start += curlyLineWidth; + } + RETURN_HR_IF(E_FAIL, !MoveToEx(_hdcMemoryContext, x, y, nullptr)); + RETURN_HR_IF(E_FAIL, !PolyBezierTo(_hdcMemoryContext, points.data(), cPoints)); + return S_OK; + }; if (lines.test(GridLines::Left)) { @@ -574,22 +596,51 @@ bool GdiEngine::FontHasWesternScript(HDC hdc) RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.gridlineWidth)); } - if (lines.any(GridLines::Underline, GridLines::DoubleUnderline)) + if (lines.test(GridLines::Strikethrough)) { - const auto y = ptTarget.y + _lineMetrics.underlineOffset; - RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.underlineWidth)); + const auto y = ptTarget.y + _lineMetrics.strikethroughOffset; + RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.strikethroughWidth)); + } - if (lines.test(GridLines::DoubleUnderline)) - { - const auto y2 = ptTarget.y + _lineMetrics.underlineOffset2; - RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y2, widthOfAllCells, _lineMetrics.underlineWidth)); - } + // Create a pen matching the underline style. + DWORD underlinePenType = PS_SOLID; + if (lines.test(GridLines::DottedUnderline)) + { + underlinePenType = PS_DOT; + } + else if (lines.test(GridLines::DashedUnderline)) + { + underlinePenType = PS_DASH; } + const LOGBRUSH brushProp{ .lbStyle = BS_SOLID, .lbColor = underlineColor }; + wil::unique_hpen hpen(ExtCreatePen(underlinePenType | PS_GEOMETRIC | PS_ENDCAP_FLAT, _lineMetrics.underlineWidth, &brushProp, 0, nullptr)); - if (lines.test(GridLines::Strikethrough)) + // Apply the pen. + const auto prevPen = wil::SelectObject(_hdcMemoryContext, hpen.get()); + RETURN_HR_IF_NULL(E_FAIL, prevPen.get()); + + const auto underlineMidY = std::lround(ptTarget.y + _lineMetrics.underlineOffset + _lineMetrics.underlineWidth / 2.0f); + if (lines.test(GridLines::Underline)) { - const auto y = ptTarget.y + _lineMetrics.strikethroughOffset; - RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.strikethroughWidth)); + return DrawStrokedLine(ptTarget.x, underlineMidY, widthOfAllCells); + } + else if (lines.test(GridLines::DoubleUnderline)) + { + const auto doubleUnderlineBottomLineMidY = std::lround(ptTarget.y + _lineMetrics.underlineOffset2 + _lineMetrics.underlineWidth / 2.0f); + RETURN_IF_FAILED(DrawStrokedLine(ptTarget.x, underlineMidY, widthOfAllCells)); + return DrawStrokedLine(ptTarget.x, doubleUnderlineBottomLineMidY, widthOfAllCells); + } + else if (lines.test(GridLines::CurlyUnderline)) + { + return DrawCurlyLine(ptTarget.x, underlineMidY, cchLine); + } + else if (lines.test(GridLines::DottedUnderline)) + { + return DrawStrokedLine(ptTarget.x, underlineMidY, widthOfAllCells); + } + else if (lines.test(GridLines::DashedUnderline)) + { + return DrawStrokedLine(ptTarget.x, underlineMidY, widthOfAllCells); } return S_OK; diff --git a/src/renderer/gdi/state.cpp b/src/renderer/gdi/state.cpp index 556f2df02fb..099c2f43f78 100644 --- a/src/renderer/gdi/state.cpp +++ b/src/renderer/gdi/state.cpp @@ -11,6 +11,15 @@ using namespace Microsoft::Console::Render; +namespace +{ + // The max height of Curly line peak in `em` units. + constexpr auto MaxCurlyLinePeakHeightEm = 0.075f; + + // The min height of Curly line peak. + constexpr auto MinCurlyLinePeakHeight = 2.0f; +} + // Routine Description: // - Creates a new GDI-based rendering engine // - NOTE: Will throw if initialization failure. Caller must catch. @@ -397,6 +406,31 @@ GdiEngine::~GdiEngine() _lineMetrics.underlineOffset2 = _lineMetrics.underlineOffset - _lineMetrics.gridlineWidth; } + // Curly line doesn't render properly below 1px stroke width. Make it a straight line. + if (_lineMetrics.underlineWidth < 1) + { + _lineMetrics.curlylinePeakHeight = 0; + } + else + { + // Curlyline uses the gap between cell bottom and singly underline + // position as the height of the wave's peak. The baseline for curly + // line is at the middle of singly underline. The gap could be too big, + // so we also apply a limit on the peak height. + const auto strokeHalfWidth = _lineMetrics.underlineWidth / 2.0f; + const auto underlineMidY = _lineMetrics.underlineOffset + strokeHalfWidth; + const auto cellBottomGap = Font.GetSize().height - underlineMidY - strokeHalfWidth; + const auto maxCurlyLinePeakHeight = MaxCurlyLinePeakHeightEm * fontSize; + auto curlyLinePeakHeight = std::min(cellBottomGap, maxCurlyLinePeakHeight); + + // When it's too small to be curly, make it a straight line. + if (curlyLinePeakHeight < MinCurlyLinePeakHeight) + { + curlyLinePeakHeight = 0.0f; + } + _lineMetrics.curlylinePeakHeight = gsl::narrow_cast(std::floor(curlyLinePeakHeight)); + } + // Now find the size of a 0 in this current font and save it for conversions done later. _coordFontLast = Font.GetSize(); diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp index 46221ae911d..afcaa5aff59 100644 --- a/src/renderer/inc/IRenderEngine.hpp +++ b/src/renderer/inc/IRenderEngine.hpp @@ -41,6 +41,9 @@ namespace Microsoft::Console::Render Right, Underline, DoubleUnderline, + CurlyUnderline, + DottedUnderline, + DashedUnderline, Strikethrough, HyperlinkUnderline }; @@ -73,7 +76,7 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT PrepareLineTransform(LineRendition lineRendition, til::CoordType targetRow, til::CoordType viewportLeft) noexcept = 0; [[nodiscard]] virtual HRESULT PaintBackground() noexcept = 0; [[nodiscard]] virtual HRESULT PaintBufferLine(std::span clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept = 0; - [[nodiscard]] virtual HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF color, size_t cchLine, til::point coordTarget) noexcept = 0; + [[nodiscard]] virtual HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF gridlineColor, COLORREF underlineColor, size_t cchLine, til::point coordTarget) noexcept = 0; [[nodiscard]] virtual HRESULT PaintSelection(const til::rect& rect) noexcept = 0; [[nodiscard]] virtual HRESULT PaintCursor(const CursorOptions& options) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, gsl::not_null pData, bool usingSoftFont, bool isSettingDefaultBrushes) noexcept = 0; diff --git a/src/renderer/inc/RenderSettings.hpp b/src/renderer/inc/RenderSettings.hpp index 4b6e7c3981c..c836bdde848 100644 --- a/src/renderer/inc/RenderSettings.hpp +++ b/src/renderer/inc/RenderSettings.hpp @@ -41,6 +41,7 @@ namespace Microsoft::Console::Render size_t GetColorAliasIndex(const ColorAlias alias) const noexcept; std::pair GetAttributeColors(const TextAttribute& attr) const noexcept; std::pair GetAttributeColorsWithAlpha(const TextAttribute& attr) const noexcept; + COLORREF GetAttributeUnderlineColor(const TextAttribute& attr) const noexcept; void ToggleBlinkRendition(class Renderer& renderer) noexcept; private: diff --git a/src/renderer/uia/UiaRenderer.cpp b/src/renderer/uia/UiaRenderer.cpp index e58c227c561..ba09384580b 100644 --- a/src/renderer/uia/UiaRenderer.cpp +++ b/src/renderer/uia/UiaRenderer.cpp @@ -347,14 +347,16 @@ void UiaEngine::WaitUntilCanRender() noexcept // For UIA, this doesn't mean anything. So do nothing. // Arguments: // - lines - -// - color - +// - gridlineColor - +// - underlineColor - // - cchLine - // - coordTarget - // Return Value: // - S_FALSE -[[nodiscard]] HRESULT UiaEngine::PaintBufferGridLines(GridLineSet const /*lines*/, - COLORREF const /*color*/, - size_t const /*cchLine*/, +[[nodiscard]] HRESULT UiaEngine::PaintBufferGridLines(const GridLineSet /*lines*/, + const COLORREF /*gridlineColor*/, + const COLORREF /*underlineColor*/, + const size_t /*cchLine*/, const til::point /*coordTarget*/) noexcept { return S_FALSE; diff --git a/src/renderer/uia/UiaRenderer.hpp b/src/renderer/uia/UiaRenderer.hpp index 5e486b7e0e1..4625ca155cb 100644 --- a/src/renderer/uia/UiaRenderer.hpp +++ b/src/renderer/uia/UiaRenderer.hpp @@ -49,7 +49,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT NotifyNewText(const std::wstring_view newText) noexcept override; [[nodiscard]] HRESULT PaintBackground() noexcept override; [[nodiscard]] HRESULT PaintBufferLine(const std::span clusters, const til::point coord, const bool fTrimLeft, const bool lineWrapped) noexcept override; - [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF color, const size_t cchLine, const til::point coordTarget) noexcept override; + [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, const gsl::not_null pData, const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept override; diff --git a/src/renderer/vt/paint.cpp b/src/renderer/vt/paint.cpp index 801390085c0..99f8853a98f 100644 --- a/src/renderer/vt/paint.cpp +++ b/src/renderer/vt/paint.cpp @@ -187,13 +187,15 @@ using namespace Microsoft::Console::Types; // - Draws up to one line worth of grid lines on top of characters. // Arguments: // - lines - Enum defining which edges of the rectangle to draw -// - color - The color to use for drawing the edges. +// - gridlineColor - The color to use for drawing the gridlines. +// - underlineColor - The color to use for drawing the underlines. // - cchLine - How many characters we should draw the grid lines along (left to right in a row) // - coordTarget - The starting X/Y position of the first character to draw on. // Return Value: // - S_OK [[nodiscard]] HRESULT VtEngine::PaintBufferGridLines(const GridLineSet /*lines*/, - const COLORREF /*color*/, + const COLORREF /*gridlineColor*/, + const COLORREF /*underlineColor*/, const size_t /*cchLine*/, const til::point /*coordTarget*/) noexcept { diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index 15de6e0e760..100e04c549d 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -62,7 +62,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition, const til::CoordType targetRow, const til::CoordType viewportLeft) noexcept override; [[nodiscard]] HRESULT PaintBackground() noexcept override; [[nodiscard]] HRESULT PaintBufferLine(std::span clusters, til::point coord, bool fTrimLeft, bool lineWrapped) noexcept override; - [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet lines, COLORREF color, size_t cchLine, til::point coordTarget) noexcept override; + [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept override; diff --git a/src/renderer/wddmcon/WddmConRenderer.cpp b/src/renderer/wddmcon/WddmConRenderer.cpp index 378b1dfd086..fda8ae6c21b 100644 --- a/src/renderer/wddmcon/WddmConRenderer.cpp +++ b/src/renderer/wddmcon/WddmConRenderer.cpp @@ -284,9 +284,10 @@ CATCH_RETURN() CATCH_RETURN(); } -[[nodiscard]] HRESULT WddmConEngine::PaintBufferGridLines(GridLineSet const /*lines*/, - COLORREF const /*color*/, - size_t const /*cchLine*/, +[[nodiscard]] HRESULT WddmConEngine::PaintBufferGridLines(const GridLineSet /*lines*/, + const COLORREF /*gridlineColor*/, + const COLORREF /*underlineColor*/, + const size_t /*cchLine*/, const til::point /*coordTarget*/) noexcept { return S_OK; diff --git a/src/renderer/wddmcon/WddmConRenderer.hpp b/src/renderer/wddmcon/WddmConRenderer.hpp index a658ef51180..954dc226946 100644 --- a/src/renderer/wddmcon/WddmConRenderer.hpp +++ b/src/renderer/wddmcon/WddmConRenderer.hpp @@ -44,7 +44,7 @@ namespace Microsoft::Console::Render const til::point coord, const bool trimLeft, const bool lineWrapped) noexcept override; - [[nodiscard]] HRESULT PaintBufferGridLines(GridLineSet const lines, COLORREF const color, size_t const cchLine, til::point const coordTarget) noexcept override; + [[nodiscard]] HRESULT PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept override; [[nodiscard]] HRESULT PaintSelection(const til::rect& rect) noexcept override; [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index af5ebb431ea..419acf1ff0b 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -405,16 +405,22 @@ std::optional UiaTextRangeBase::_verifyAttr(TEXTATTRIBUTEID attributeId, V THROW_HR_IF(E_INVALIDARG, val.vt != VT_I4); // The underline style is stored as a TextDecorationLineStyle. - // However, The text buffer doesn't have that many different styles for being underlined. - // Instead, we only have single and double underlined. + // However, The text buffer doesn't have all the different styles for being underlined. + // Instead, we only use a subset of them. switch (val.lVal) { case TextDecorationLineStyle_None: return !attr.IsUnderlined(); + case TextDecorationLineStyle_Single: + return attr.GetUnderlineStyle() == UnderlineStyle::SinglyUnderlined; case TextDecorationLineStyle_Double: return attr.GetUnderlineStyle() == UnderlineStyle::DoublyUnderlined; - case TextDecorationLineStyle_Single: // singly underlined and extended styles are treated the same - return attr.IsUnderlined() && attr.GetUnderlineStyle() != UnderlineStyle::DoublyUnderlined; + case TextDecorationLineStyle_Wavy: + return attr.GetUnderlineStyle() == UnderlineStyle::CurlyUnderlined; + case TextDecorationLineStyle_Dot: + return attr.GetUnderlineStyle() == UnderlineStyle::DottedUnderlined; + case TextDecorationLineStyle_Dash: + return attr.GetUnderlineStyle() == UnderlineStyle::DashedUnderlined; default: return std::nullopt; } @@ -697,18 +703,26 @@ bool UiaTextRangeBase::_initializeAttrQuery(TEXTATTRIBUTEID attributeId, VARIANT const auto style = attr.GetUnderlineStyle(); switch (style) { + case UnderlineStyle::NoUnderline: + pRetVal->lVal = TextDecorationLineStyle_None; + return true; case UnderlineStyle::SinglyUnderlined: pRetVal->lVal = TextDecorationLineStyle_Single; return true; case UnderlineStyle::DoublyUnderlined: pRetVal->lVal = TextDecorationLineStyle_Double; return true; - case UnderlineStyle::NoUnderline: - pRetVal->lVal = TextDecorationLineStyle_None; + case UnderlineStyle::CurlyUnderlined: + pRetVal->lVal = TextDecorationLineStyle_Wavy; + return true; + case UnderlineStyle::DottedUnderlined: + pRetVal->lVal = TextDecorationLineStyle_Dot; + return true; + case UnderlineStyle::DashedUnderlined: + pRetVal->lVal = TextDecorationLineStyle_Dash; return true; + // Out of range styles are treated as singly underlined. default: - // TODO: Handle other underline styles once they're supported in the graphic renderer. - // For now, extended styles are treated (and rendered) as single underline. pRetVal->lVal = TextDecorationLineStyle_Single; return true; }