From eb349935a0c8c6110781605d42c66de8a65c1009 Mon Sep 17 00:00:00 2001 From: Chester Liu Date: Thu, 18 Feb 2021 14:11:38 +0800 Subject: [PATCH] Introduce DxFontRenderData (#9096) This is my attempt to isolate all the dwrite font related thing by introducing a new layer - `DxFontRenderData`. This will free `DxRenderer` & `CustomTextLayout` from the burden of handling fonts & box effects. The logic is more simplified & streamlined. In short I just moved everything fonts-related into `DxFontRenderData` and started from there. There's no modification to code logic. Just pure structural stuff. SGR support tracking issue: #6879 Initial Italic support PR: #8580 --- src/renderer/base/lib/base.vcxproj | 2 +- src/renderer/dx/CustomTextLayout.cpp | 311 +--------- src/renderer/dx/CustomTextLayout.h | 25 +- src/renderer/dx/DxFontRenderData.cpp | 755 +++++++++++++++++++++++++ src/renderer/dx/DxFontRenderData.h | 96 ++++ src/renderer/dx/DxRenderer.cpp | 561 ++---------------- src/renderer/dx/DxRenderer.hpp | 48 +- src/renderer/dx/lib/dx.vcxproj | 2 + src/renderer/dx/lib/dx.vcxproj.filters | 2 + src/renderer/dx/sources.inc | 1 + 10 files changed, 928 insertions(+), 875 deletions(-) create mode 100644 src/renderer/dx/DxFontRenderData.cpp create mode 100644 src/renderer/dx/DxFontRenderData.h diff --git a/src/renderer/base/lib/base.vcxproj b/src/renderer/base/lib/base.vcxproj index 434f25d2907..f38398cfc9c 100644 --- a/src/renderer/base/lib/base.vcxproj +++ b/src/renderer/base/lib/base.vcxproj @@ -6,7 +6,7 @@ base RendererBase ConRenderBase - StaticLibrary + StaticLibrary diff --git a/src/renderer/dx/CustomTextLayout.cpp b/src/renderer/dx/CustomTextLayout.cpp index 41a677c7bbb..f051026d551 100644 --- a/src/renderer/dx/CustomTextLayout.cpp +++ b/src/renderer/dx/CustomTextLayout.cpp @@ -17,43 +17,21 @@ using namespace Microsoft::Console::Render; // Routine Description: // - Creates a CustomTextLayout object for calculating which glyphs should be placed and where // Arguments: -// - factory - DirectWrite factory reference in case we need other DirectWrite objects for our layout -// - analyzer - DirectWrite text analyzer from the factory that has been cached at a level above this layout (expensive to create) -// - format - The DirectWrite format object representing the size and other text properties to be applied (by default) to a layout -// - formatItalic - The italic variant of the format object representing the size and other text properties for italic text -// - font - The DirectWrite font face to use while calculating layout (by default, will fallback if necessary) -// - fontItalic - The italic variant of the font face to use while calculating layout for italic text -// - width - The count of pixels available per column (the expected pixel width of every column) -// - boxEffect - Box drawing scaling effects that are cached for the base font across layouts. -CustomTextLayout::CustomTextLayout(gsl::not_null const factory, - gsl::not_null const analyzer, - gsl::not_null const format, - gsl::not_null const formatItalic, - gsl::not_null const font, - gsl::not_null const fontItalic, - size_t const width, - IBoxDrawingEffect* const boxEffect) : - _factory{ factory.get() }, - _analyzer{ analyzer.get() }, - _format{ format.get() }, - _formatItalic{ formatItalic.get() }, - _formatInUse{ format.get() }, - _font{ font.get() }, - _fontItalic{ fontItalic.get() }, - _fontInUse{ font.get() }, - _boxDrawingEffect{ boxEffect }, - _localeName{}, +// - dxFontRenderData - The DirectWrite font render data for our layout +CustomTextLayout::CustomTextLayout(gsl::not_null const fontRenderData) : + _fontRenderData{ fontRenderData }, + _formatInUse{ fontRenderData->DefaultTextFormat().Get() }, + _fontInUse{ fontRenderData->DefaultFontFace().Get() }, _numberSubstitution{}, _readingDirection{ DWRITE_READING_DIRECTION_LEFT_TO_RIGHT }, _runs{}, _breakpoints{}, _runIndex{ 0 }, - _width{ width }, + _width{ gsl::narrow_cast(fontRenderData->GlyphCell().width()) }, _isEntireTextSimple{ false } { - // Fetch the locale name out once now from the format - _localeName.resize(gsl::narrow_cast(format->GetLocaleNameLength()) + 1); // +1 for null - THROW_IF_FAILED(format->GetLocaleName(_localeName.data(), gsl::narrow(_localeName.size()))); + _localeName.resize(gsl::narrow_cast(fontRenderData->DefaultTextFormat()->GetLocaleNameLength()) + 1); // +1 for null + THROW_IF_FAILED(fontRenderData->DefaultTextFormat()->GetLocaleName(_localeName.data(), gsl::narrow(_localeName.size()))); } //Routine Description: @@ -122,8 +100,8 @@ CATCH_RETURN() RETURN_HR_IF_NULL(E_INVALIDARG, columns); *columns = 0; - _formatInUse = _format.Get(); - _fontInUse = _font.Get(); + _formatInUse = _fontRenderData->DefaultTextFormat().Get(); + _fontInUse = _fontRenderData->DefaultFontFace().Get(); RETURN_IF_FAILED(_AnalyzeTextComplexity()); RETURN_IF_FAILED(_AnalyzeRuns()); @@ -157,8 +135,8 @@ CATCH_RETURN() FLOAT originY) noexcept { const auto drawingContext = static_cast(clientDrawingContext); - _formatInUse = drawingContext->useItalicFont ? _formatItalic.Get() : _format.Get(); - _fontInUse = drawingContext->useItalicFont ? _fontItalic.Get() : _font.Get(); + _formatInUse = drawingContext->useItalicFont ? _fontRenderData->ItalicTextFormat().Get() : _fontRenderData->DefaultTextFormat().Get(); + _fontInUse = drawingContext->useItalicFont ? _fontRenderData->ItalicFontFace().Get() : _fontRenderData->DefaultFontFace().Get(); RETURN_IF_FAILED(_AnalyzeTextComplexity()); RETURN_IF_FAILED(_AnalyzeRuns()); @@ -196,7 +174,7 @@ CATCH_RETURN() _glyphIndices.resize(textLength); - const HRESULT hr = _analyzer->GetTextComplexity( + const HRESULT hr = _fontRenderData->Analyzer()->GetTextComplexity( _text.c_str(), textLength, _fontInUse, @@ -243,10 +221,10 @@ CATCH_RETURN() if (!_isEntireTextSimple) { // Call each of the analyzers in sequence, recording their results. - RETURN_IF_FAILED(_analyzer->AnalyzeLineBreakpoints(this, 0, textLength, this)); - RETURN_IF_FAILED(_analyzer->AnalyzeBidi(this, 0, textLength, this)); - RETURN_IF_FAILED(_analyzer->AnalyzeScript(this, 0, textLength, this)); - RETURN_IF_FAILED(_analyzer->AnalyzeNumberSubstitution(this, 0, textLength, this)); + RETURN_IF_FAILED(_fontRenderData->Analyzer()->AnalyzeLineBreakpoints(this, 0, textLength, this)); + RETURN_IF_FAILED(_fontRenderData->Analyzer()->AnalyzeBidi(this, 0, textLength, this)); + RETURN_IF_FAILED(_fontRenderData->Analyzer()->AnalyzeScript(this, 0, textLength, this)); + RETURN_IF_FAILED(_fontRenderData->Analyzer()->AnalyzeNumberSubstitution(this, 0, textLength, this)); // Perform our custom font fallback analyzer that mimics the pattern of the real analyzers. RETURN_IF_FAILED(_AnalyzeFontFallback(this, 0, textLength)); } @@ -407,7 +385,7 @@ CATCH_RETURN() HRESULT hr = S_OK; do { - hr = _analyzer->GetGlyphs( + hr = _fontRenderData->Analyzer()->GetGlyphs( &_text.at(textStart), textLength, run.fontFace.Get(), @@ -452,7 +430,7 @@ CATCH_RETURN() const auto fontSizeFormat = _formatInUse->GetFontSize(); const auto fontSize = fontSizeFormat * run.fontScale; - hr = _analyzer->GetGlyphPlacements( + hr = _fontRenderData->Analyzer()->GetGlyphPlacements( &_text.at(textStart), &_glyphClusters.at(textStart), &textProps.at(0), @@ -1265,9 +1243,7 @@ CATCH_RETURN(); if (!fallback) { - ::Microsoft::WRL::ComPtr factory2; - RETURN_IF_FAILED(_factory.As(&factory2)); - factory2->GetSystemFontFallback(&fallback); + fallback = _fontRenderData->SystemFontFallback(); } // Walk through and analyze the entire string @@ -1467,14 +1443,14 @@ try { auto& run = _FetchNextRun(textLength); - if (run.fontFace == _font) + if (run.fontFace == _fontRenderData->DefaultFontFace()) { - run.drawingEffect = _boxDrawingEffect; + run.drawingEffect = _fontRenderData->DefaultBoxDrawingEffect(); } else { ::Microsoft::WRL::ComPtr eff; - RETURN_IF_FAILED(s_CalculateBoxEffect(_formatInUse, _width, run.fontFace.Get(), run.fontScale, &eff)); + RETURN_IF_FAILED(DxFontRenderData::s_CalculateBoxEffect(_formatInUse, _width, run.fontFace.Get(), run.fontScale, &eff)); // store data in the run run.drawingEffect = std::move(eff); @@ -1485,247 +1461,6 @@ try } CATCH_RETURN(); -// Routine Description: -// - Calculates the box drawing scale/translate matrix values to fit a box glyph into the cell as perfectly as possible. -// Arguments: -// - format - Text format used to determine line spacing (height including ascent & descent) as calculated from the base font. -// - widthPixels - The pixel width of the available cell. -// - face - The font face that is currently being used, may differ from the base font from the layout. -// - fontScale - if the given font face is going to be scaled versus the format, we need to know so we can compensate for that. pass 1.0f for no scaling. -// - effect - Receives the effect to apply to box drawing characters. If no effect is received, special treatment isn't required. -// Return Value: -// - S_OK, GSL/WIL errors, DirectWrite errors, or math errors. -[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept -try -{ - // Check for bad in parameters. - RETURN_HR_IF(E_INVALIDARG, !format); - RETURN_HR_IF(E_INVALIDARG, !face); - - // Check the out parameter and fill it up with null. - RETURN_HR_IF(E_INVALIDARG, !effect); - *effect = nullptr; - - // The format is based around the main font that was specified by the user. - // We need to know its size as well as the final spacing that was calculated around - // it when it was first selected to get an idea of how large the bounding box is. - const auto fontSize = format->GetFontSize(); - - DWRITE_LINE_SPACING_METHOD spacingMethod; - float lineSpacing; // total height of the cells - float baseline; // vertical position counted down from the top where the characters "sit" - RETURN_IF_FAILED(format->GetLineSpacing(&spacingMethod, &lineSpacing, &baseline)); - - const float ascentPixels = baseline; - const float descentPixels = lineSpacing - baseline; - - // We need this for the designUnitsPerEm which will be required to move back and forth between - // Design Units and Pixels. I'll elaborate below. - DWRITE_FONT_METRICS1 fontMetrics; - face->GetMetrics(&fontMetrics); - - // If we had font fallback occur, the size of the font given to us (IDWriteFontFace1) can be different - // than the font size used for the original format (IDWriteTextFormat). - const auto scaledFontSize = fontScale * fontSize; - - // This is Unicode FULL BLOCK U+2588. - // We presume that FULL BLOCK should be filling its entire cell in all directions so it should provide a good basis - // in knowing exactly where to touch every single edge. - // We're also presuming that the other box/line drawing glyphs were authored in this font to perfectly inscribe - // inside of FULL BLOCK, with the same left/top/right/bottom bearings so they would look great when drawn adjacent. - const UINT32 blockCodepoint = L'\x2588'; - - // Get the index of the block out of the font. - UINT16 glyphIndex; - RETURN_IF_FAILED(face->GetGlyphIndicesW(&blockCodepoint, 1, &glyphIndex)); - - // If it was 0, it wasn't found in the font. We're going to try again with - // Unicode BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL U+253C which should be touching - // all the edges of the possible rectangle, much like a full block should. - if (glyphIndex == 0) - { - const UINT32 alternateCp = L'\x253C'; - RETURN_IF_FAILED(face->GetGlyphIndicesW(&alternateCp, 1, &glyphIndex)); - } - - // If we still didn't find the glyph index, we haven't implemented any further logic to figure out the box dimensions. - // So we're just going to leave successfully as is and apply no scaling factor. It might look not-right, but it won't - // stop the rendering pipeline. - RETURN_HR_IF(S_FALSE, glyphIndex == 0); - - // Get the metrics of the given glyph, which we're going to treat as the outline box in which all line/block drawing - // glyphs will be inscribed within, perfectly touching each edge as to align when two cells meet. - DWRITE_GLYPH_METRICS boxMetrics = { 0 }; - RETURN_IF_FAILED(face->GetDesignGlyphMetrics(&glyphIndex, 1, &boxMetrics)); - - // NOTE: All metrics we receive from DWRITE are going to be in "design units" which are a somewhat agnostic - // way of describing proportions. - // Converting back and forth between real pixels and design units is possible using - // any font's specific fontSize and the designUnitsPerEm FONT_METRIC value. - // - // Here's what to know about the boxMetrics: - // - // - // - // topLeft --> +--------------------------------+ --- - // | ^ | | - // | | topSide | | - // | | Bearing | | - // | v | | - // | +-----------------+ | | - // | | | | | - // | | | | | a - // | | | | | d - // | | | | | v - // +<---->+ | | | a - // | | | | | n - // | left | | | | c - // | Side | | | | e - // | Bea- | | | | H - // | ring | | right | | e - // vertical | | | Side | | i - // OriginY --> x | | Bea- | | g - // | | | ring | | h - // | | | | | t - // | | +<----->+ | - // | +-----------------+ | | - // | ^ | | - // | bottomSide | | | - // | Bearing | | | - // | v | | - // +--------------------------------+ --- - // - // - // | | - // +--------------------------------+ - // | advanceWidth | - // - // - // NOTE: The bearings can be negative, in which case it is specifying that the glyphs overhang the box - // as defined by the advanceHeight/width. - // See also: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics - - // The scale is a multiplier and the translation is addition. So *1 and +0 will mean nothing happens. - const float defaultBoxVerticalScaleFactor = 1.0f; - float boxVerticalScaleFactor = defaultBoxVerticalScaleFactor; - const float defaultBoxVerticalTranslation = 0.0f; - float boxVerticalTranslation = defaultBoxVerticalTranslation; - { - // First, find the dimensions of the glyph representing our fully filled box. - - // Ascent is how far up from the baseline we'll draw. - // verticalOriginY is the measure from the topLeft corner of the bounding box down to where - // the glyph's version of the baseline is. - // topSideBearing is how much "gap space" is left between that topLeft and where the glyph - // starts drawing. Subtract the gap space to find how far is drawn upward from baseline. - const auto boxAscentDesignUnits = boxMetrics.verticalOriginY - boxMetrics.topSideBearing; - - // Descent is how far down from the baseline we'll draw. - // advanceHeight is the total height of the drawn bounding box. - // verticalOriginY is how much was given to the ascent, so subtract that out. - // What remains is then the descent value. Remove the - // bottomSideBearing as the "gap space" on the bottom to find how far is drawn downward from baseline. - const auto boxDescentDesignUnits = boxMetrics.advanceHeight - boxMetrics.verticalOriginY - boxMetrics.bottomSideBearing; - - // The height, then, of the entire box is just the sum of the ascent above the baseline and the descent below. - const auto boxHeightDesignUnits = boxAscentDesignUnits + boxDescentDesignUnits; - - // Second, find the dimensions of the cell we're going to attempt to fit within. - // We know about the exact ascent/descent units in pixels as calculated when we chose a font and - // adjusted the ascent/descent for a nice perfect baseline and integer total height. - // All we need to do is adapt it into Design Units so it meshes nicely with the Design Units above. - // Use the formula: Pixels * Design Units Per Em / Font Size = Design Units - const auto cellAscentDesignUnits = ascentPixels * fontMetrics.designUnitsPerEm / scaledFontSize; - const auto cellDescentDesignUnits = descentPixels * fontMetrics.designUnitsPerEm / scaledFontSize; - const auto cellHeightDesignUnits = cellAscentDesignUnits + cellDescentDesignUnits; - - // OK, now do a few checks. If the drawn box touches the top and bottom of the cell - // and the box is overall tall enough, then we'll not bother adjusting. - // We will presume the font author has set things as they wish them to be. - const auto boxTouchesCellTop = boxAscentDesignUnits >= cellAscentDesignUnits; - const auto boxTouchesCellBottom = boxDescentDesignUnits >= cellDescentDesignUnits; - const auto boxIsTallEnoughForCell = boxHeightDesignUnits >= cellHeightDesignUnits; - - // If not... - if (!(boxTouchesCellTop && boxTouchesCellBottom && boxIsTallEnoughForCell)) - { - // Find a scaling factor that will make the total height drawn of this box - // perfectly fit the same number of design units as the cell. - // Since scale factor is a multiplier, it doesn't matter that this is design units. - // The fraction between the two heights in pixels should be exactly the same - // (which is what will matter when we go to actually render it... the pixels that is.) - // Don't scale below 1.0. If it'd shrink, just center it at the prescribed scale. - boxVerticalScaleFactor = std::max(cellHeightDesignUnits / boxHeightDesignUnits, 1.0f); - - // The box as scaled might be hanging over the top or bottom of the cell (or both). - // We find out the amount of overhang/underhang on both the top and the bottom. - const auto extraAscent = boxAscentDesignUnits * boxVerticalScaleFactor - cellAscentDesignUnits; - const auto extraDescent = boxDescentDesignUnits * boxVerticalScaleFactor - cellDescentDesignUnits; - - // This took a bit of time and effort and it's difficult to put into words, but here goes. - // We want the average of the two magnitudes to find out how much to "take" from one and "give" - // to the other such that both are equal. We presume the glyphs are designed to be drawn - // centered in their box vertically to look good. - // The ordering around subtraction is required to ensure that the direction is correct with a negative - // translation moving up (taking excess descent and adding to ascent) and positive is the opposite. - const auto boxVerticalTranslationDesignUnits = (extraAscent - extraDescent) / 2; - - // The translation is just a raw movement of pixels up or down. Since we were working in Design Units, - // we need to run the opposite algorithm shown above to go from Design Units to Pixels. - boxVerticalTranslation = boxVerticalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm; - } - } - - // The horizontal adjustments follow the exact same logic as the vertical ones. - const float defaultBoxHorizontalScaleFactor = 1.0f; - float boxHorizontalScaleFactor = defaultBoxHorizontalScaleFactor; - const float defaultBoxHorizontalTranslation = 0.0f; - float boxHorizontalTranslation = defaultBoxHorizontalTranslation; - { - // This is the only difference. We don't have a horizontalOriginX from the metrics. - // However, https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics says - // the X coordinate is specified by half the advanceWidth to the right of the horizontalOrigin. - // So we'll use that as the "center" and apply it the role that verticalOriginY had above. - - const auto boxCenterDesignUnits = boxMetrics.advanceWidth / 2; - const auto boxLeftDesignUnits = boxCenterDesignUnits - boxMetrics.leftSideBearing; - const auto boxRightDesignUnits = boxMetrics.advanceWidth - boxMetrics.rightSideBearing - boxCenterDesignUnits; - const auto boxWidthDesignUnits = boxLeftDesignUnits + boxRightDesignUnits; - - const auto cellWidthDesignUnits = widthPixels * fontMetrics.designUnitsPerEm / scaledFontSize; - const auto cellLeftDesignUnits = cellWidthDesignUnits / 2; - const auto cellRightDesignUnits = cellLeftDesignUnits; - - const auto boxTouchesCellLeft = boxLeftDesignUnits >= cellLeftDesignUnits; - const auto boxTouchesCellRight = boxRightDesignUnits >= cellRightDesignUnits; - const auto boxIsWideEnoughForCell = boxWidthDesignUnits >= cellWidthDesignUnits; - - if (!(boxTouchesCellLeft && boxTouchesCellRight && boxIsWideEnoughForCell)) - { - boxHorizontalScaleFactor = std::max(cellWidthDesignUnits / boxWidthDesignUnits, 1.0f); - const auto extraLeft = boxLeftDesignUnits * boxHorizontalScaleFactor - cellLeftDesignUnits; - const auto extraRight = boxRightDesignUnits * boxHorizontalScaleFactor - cellRightDesignUnits; - - const auto boxHorizontalTranslationDesignUnits = (extraLeft - extraRight) / 2; - - boxHorizontalTranslation = boxHorizontalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm; - } - } - - // If we set anything, make a drawing effect. Otherwise, there isn't one. - if (defaultBoxVerticalScaleFactor != boxVerticalScaleFactor || - defaultBoxVerticalTranslation != boxVerticalTranslation || - defaultBoxHorizontalScaleFactor != boxHorizontalScaleFactor || - defaultBoxHorizontalTranslation != boxHorizontalTranslation) - { - // OK, make the object that will represent our effect, stuff the metrics into it, and return it. - RETURN_IF_FAILED(WRL::MakeAndInitialize(effect, boxVerticalScaleFactor, boxVerticalTranslation, boxHorizontalScaleFactor, boxHorizontalTranslation)); - } - - return S_OK; -} -CATCH_RETURN() - #pragma endregion #pragma region internal Run manipulation functions for storing information from sink callbacks diff --git a/src/renderer/dx/CustomTextLayout.h b/src/renderer/dx/CustomTextLayout.h index ca9fd9b9651..2ac04278bf1 100644 --- a/src/renderer/dx/CustomTextLayout.h +++ b/src/renderer/dx/CustomTextLayout.h @@ -11,6 +11,7 @@ #include #include "BoxDrawingEffect.h" +#include "DxFontRenderData.h" #include "../inc/Cluster.hpp" namespace Microsoft::Console::Render @@ -20,14 +21,7 @@ namespace Microsoft::Console::Render public: // Based on the Windows 7 SDK sample at https://github.com/pauldotknopf/WindowsSDK7-Samples/tree/master/multimedia/DirectWrite/CustomLayout - CustomTextLayout(gsl::not_null const factory, - gsl::not_null const analyzer, - gsl::not_null const normalFormat, - gsl::not_null const italicFormat, - gsl::not_null const normalFont, - gsl::not_null const italicFont, - size_t const width, - IBoxDrawingEffect* const boxEffect); + CustomTextLayout(gsl::not_null const fontRenderData); [[nodiscard]] HRESULT STDMETHODCALLTYPE AppendClusters(const gsl::span clusters); @@ -71,8 +65,6 @@ namespace Microsoft::Console::Render UINT32 textLength, _In_ IDWriteNumberSubstitution* numberSubstitution) override; - [[nodiscard]] static HRESULT STDMETHODCALLTYPE s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept; - protected: // A single contiguous run of characters containing the same analysis results. struct Run @@ -157,24 +149,15 @@ namespace Microsoft::Console::Render [[nodiscard]] static constexpr UINT32 _EstimateGlyphCount(const UINT32 textLength) noexcept; private: - const ::Microsoft::WRL::ComPtr _factory; - - // DirectWrite analyzer - const ::Microsoft::WRL::ComPtr _analyzer; + // DirectWrite font render data + DxFontRenderData* _fontRenderData; // DirectWrite text formats - const ::Microsoft::WRL::ComPtr _format; - const ::Microsoft::WRL::ComPtr _formatItalic; IDWriteTextFormat* _formatInUse; // DirectWrite font faces - const ::Microsoft::WRL::ComPtr _font; - const ::Microsoft::WRL::ComPtr _fontItalic; IDWriteFontFace1* _fontInUse; - // Box drawing effect - const ::Microsoft::WRL::ComPtr _boxDrawingEffect; - // The text we're analyzing and processing into a layout std::wstring _text; std::vector _textClusterColumns; diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp new file mode 100644 index 00000000000..0c431f2bf4c --- /dev/null +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -0,0 +1,755 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "DxFontRenderData.h" + +static constexpr float POINTS_PER_INCH = 72.0f; +static constexpr std::wstring_view FALLBACK_FONT_FACES[] = { L"Consolas", L"Lucida Console", L"Courier New" }; +static constexpr std::wstring_view FALLBACK_LOCALE = L"en-us"; + +using namespace Microsoft::Console::Render; + +DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwriteFactory) noexcept : + _dwriteFactory(dwriteFactory), + _glyphCell{}, + _lineMetrics({}), + _boxDrawingEffect{} +{ +} + +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::Analyzer() noexcept +{ + return _dwriteTextAnalyzer; +} + +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::SystemFontFallback() +{ + if (!_systemFontFallback) + { + ::Microsoft::WRL::ComPtr factory2; + THROW_IF_FAILED(_dwriteFactory.As(&factory2)); + factory2->GetSystemFontFallback(&_systemFontFallback); + } + + return _systemFontFallback; +} + +[[nodiscard]] til::size DxFontRenderData::GlyphCell() noexcept +{ + return _glyphCell; +} + +[[nodiscard]] DxFontRenderData::LineMetrics DxFontRenderData::GetLineMetrics() noexcept +{ + return _lineMetrics; +} + +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultTextFormat() noexcept +{ + return _dwriteTextFormat; +} + +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultFontFace() noexcept +{ + return _dwriteFontFace; +} + +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultBoxDrawingEffect() noexcept +{ + return _boxDrawingEffect; +} + +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::ItalicTextFormat() noexcept +{ + return _dwriteTextFormatItalic; +} + +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::ItalicFontFace() noexcept +{ + return _dwriteFontFaceItalic; +} + +// Routine Description: +// - Updates the font used for drawing +// Arguments: +// - desired - Information specifying the font that is requested +// - actual - Filled with the nearest font actually chosen for drawing +// - dpi - The DPI of the screen +// Return Value: +// - S_OK or relevant DirectX error +[[nodiscard]] HRESULT DxFontRenderData::UpdateFont(const FontInfoDesired& desired, FontInfo& actual, const int dpi) noexcept +{ + try + { + _userLocaleName.clear(); + + std::wstring fontName(desired.GetFaceName()); + DWRITE_FONT_WEIGHT weight = static_cast(desired.GetWeight()); + DWRITE_FONT_STYLE style = DWRITE_FONT_STYLE_NORMAL; + DWRITE_FONT_STRETCH stretch = DWRITE_FONT_STRETCH_NORMAL; + std::wstring localeName = _GetUserLocaleName(); + + // _ResolveFontFaceWithFallback overrides the last argument with the locale name of the font, + // but we should use the system's locale to render the text. + std::wstring fontLocaleName = localeName; + const auto face = _ResolveFontFaceWithFallback(fontName, weight, stretch, style, fontLocaleName); + + DWRITE_FONT_METRICS1 fontMetrics; + face->GetMetrics(&fontMetrics); + + const UINT32 spaceCodePoint = L'M'; + UINT16 spaceGlyphIndex; + THROW_IF_FAILED(face->GetGlyphIndicesW(&spaceCodePoint, 1, &spaceGlyphIndex)); + + INT32 advanceInDesignUnits; + THROW_IF_FAILED(face->GetDesignGlyphAdvances(1, &spaceGlyphIndex, &advanceInDesignUnits)); + + DWRITE_GLYPH_METRICS spaceMetrics = { 0 }; + THROW_IF_FAILED(face->GetDesignGlyphMetrics(&spaceGlyphIndex, 1, &spaceMetrics)); + + // The math here is actually: + // Requested Size in Points * DPI scaling factor * Points to Pixels scaling factor. + // - DPI = dots per inch + // - PPI = points per inch or "points" as usually seen when choosing a font size + // - The DPI scaling factor is the current monitor DPI divided by 96, the default DPI. + // - The Points to Pixels factor is based on the typography definition of 72 points per inch. + // As such, converting requires taking the 96 pixel per inch default and dividing by the 72 points per inch + // to get a factor of 1 and 1/3. + // This turns into something like: + // - 12 ppi font * (96 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 16 pixels tall font for 100% display (96 dpi is 100%) + // - 12 ppi font * (144 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 24 pixels tall font for 150% display (144 dpi is 150%) + // - 12 ppi font * (192 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 32 pixels tall font for 200% display (192 dpi is 200%) + float heightDesired = static_cast(desired.GetEngineSize().Y) * static_cast(USER_DEFAULT_SCREEN_DPI) / POINTS_PER_INCH; + + // The advance is the number of pixels left-to-right (X dimension) for the given font. + // We're finding a proportional factor here with the design units in "ems", not an actual pixel measurement. + + // Now we play trickery with the font size. Scale by the DPI to get the height we expect. + heightDesired *= (static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI)); + + const float widthAdvance = static_cast(advanceInDesignUnits) / fontMetrics.designUnitsPerEm; + + // Use the real pixel height desired by the "em" factor for the width to get the number of pixels + // we will need per character in width. This will almost certainly result in fractional X-dimension pixels. + const float widthApprox = heightDesired * widthAdvance; + + // Since we can't deal with columns of the presentation grid being fractional pixels in width, round to the nearest whole pixel. + const float widthExact = round(widthApprox); + + // Now reverse the "em" factor from above to turn the exact pixel width into a (probably) fractional + // height in pixels of each character. It's easier for us to pad out height and align vertically + // than it is horizontally. + const auto fontSize = widthExact / widthAdvance; + + // Now figure out the basic properties of the character height which include ascent and descent + // for this specific font size. + const float ascent = (fontSize * fontMetrics.ascent) / fontMetrics.designUnitsPerEm; + const float descent = (fontSize * fontMetrics.descent) / fontMetrics.designUnitsPerEm; + + // Get the gap. + const float gap = (fontSize * fontMetrics.lineGap) / fontMetrics.designUnitsPerEm; + const float halfGap = gap / 2; + + // We're going to build a line spacing object here to track all of this data in our format. + DWRITE_LINE_SPACING lineSpacing = {}; + lineSpacing.method = DWRITE_LINE_SPACING_METHOD_UNIFORM; + + // We need to make sure the baseline falls on a round pixel (not a fractional pixel). + // If the baseline is fractional, the text appears blurry, especially at small scales. + // Since we also need to make sure the bounding box as a whole is round pixels + // (because the entire console system maths in full cell units), + // we're just going to ceiling up the ascent and descent to make a full pixel amount + // and set the baseline to the full round pixel ascent value. + // + // For reference, for the letters "ag": + // ... + // gggggg bottom of previous line + // + // ----------------- <===========================================| + // | topSideBearing | 1/2 lineGap | + // aaaaaa ggggggg <-------------------------|-------------| | + // a g g | | | + // aaaaa ggggg |<-ascent | | + // a a g | | |---- lineHeight + // aaaaa a gggggg <----baseline, verticalOriginY----------|---| + // g g |<-descent | | + // gggggg <-------------------------|-------------| | + // | bottomSideBearing | 1/2 lineGap | + // ----------------- <===========================================| + // + // aaaaaa ggggggg top of next line + // ... + // + // Also note... + // We're going to add half the line gap to the ascent and half the line gap to the descent + // to ensure that the spacing is balanced vertically. + // Generally speaking, the line gap is added to the ascent by DirectWrite itself for + // horizontally drawn text which can place the baseline and glyphs "lower" in the drawing + // box than would be desired for proper alignment of things like line and box characters + // which will try to sit centered in the area and touch perfectly with their neighbors. + + const auto fullPixelAscent = ceil(ascent + halfGap); + const auto fullPixelDescent = ceil(descent + halfGap); + lineSpacing.height = fullPixelAscent + fullPixelDescent; + lineSpacing.baseline = fullPixelAscent; + + // According to MSDN (https://docs.microsoft.com/en-us/windows/win32/api/dwrite_3/ne-dwrite_3-dwrite_font_line_gap_usage) + // Setting "ENABLED" means we've included the line gapping in the spacing numbers given. + lineSpacing.fontLineGapUsage = DWRITE_FONT_LINE_GAP_USAGE_ENABLED; + + // Create the font with the fractional pixel height size. + // It should have an integer pixel width by our math above. + // Then below, apply the line spacing to the format to position the floating point pixel height characters + // into a cell that has an integer pixel height leaving some padding above/below as necessary to round them out. + Microsoft::WRL::ComPtr format; + THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontName.data(), + nullptr, + weight, + style, + stretch, + fontSize, + localeName.data(), + &format)); + + THROW_IF_FAILED(format.As(&_dwriteTextFormat)); + + // We also need to create an italic variant of the font face and text + // format, based on the same parameters, but using an italic style. + std::wstring fontNameItalic = fontName; + DWRITE_FONT_WEIGHT weightItalic = weight; + DWRITE_FONT_STYLE styleItalic = DWRITE_FONT_STYLE_ITALIC; + DWRITE_FONT_STRETCH stretchItalic = stretch; + + const auto faceItalic = _ResolveFontFaceWithFallback(fontNameItalic, weightItalic, stretchItalic, styleItalic, fontLocaleName); + + Microsoft::WRL::ComPtr formatItalic; + THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontNameItalic.data(), + nullptr, + weightItalic, + styleItalic, + stretchItalic, + fontSize, + localeName.data(), + &formatItalic)); + + THROW_IF_FAILED(formatItalic.As(&_dwriteTextFormatItalic)); + + Microsoft::WRL::ComPtr analyzer; + THROW_IF_FAILED(_dwriteFactory->CreateTextAnalyzer(&analyzer)); + THROW_IF_FAILED(analyzer.As(&_dwriteTextAnalyzer)); + + _dwriteFontFace = face; + _dwriteFontFaceItalic = faceItalic; + + THROW_IF_FAILED(_dwriteTextFormat->SetLineSpacing(lineSpacing.method, lineSpacing.height, lineSpacing.baseline)); + THROW_IF_FAILED(_dwriteTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR)); + THROW_IF_FAILED(_dwriteTextFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP)); + + // The scaled size needs to represent the pixel box that each character will fit within for the purposes + // of hit testing math and other such multiplication/division. + COORD coordSize = { 0 }; + coordSize.X = gsl::narrow(widthExact); + coordSize.Y = gsl::narrow_cast(lineSpacing.height); + + // Unscaled is for the purposes of re-communicating this font back to the renderer again later. + // As such, we need to give the same original size parameter back here without padding + // or rounding or scaling manipulation. + const COORD unscaled = desired.GetEngineSize(); + + const COORD scaled = coordSize; + + actual.SetFromEngine(fontName, + desired.GetFamily(), + _dwriteTextFormat->GetFontWeight(), + false, + scaled, + unscaled); + + LineMetrics lineMetrics; + // 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. + lineMetrics.gridlineWidth = std::round(fontSize * 0.025f); + + // All other line metrics are in design units, so to get a pixel value, + // we scale by the font size divided by the design-units-per-em. + const auto scale = fontSize / fontMetrics.designUnitsPerEm; + lineMetrics.underlineOffset = std::round(fontMetrics.underlinePosition * scale); + lineMetrics.underlineWidth = std::round(fontMetrics.underlineThickness * scale); + lineMetrics.strikethroughOffset = std::round(fontMetrics.strikethroughPosition * scale); + lineMetrics.strikethroughWidth = std::round(fontMetrics.strikethroughThickness * scale); + + // We always want the lines to be visible, so if a stroke width ends up + // at zero after rounding, we need to make it at least 1 pixel. + lineMetrics.gridlineWidth = std::max(lineMetrics.gridlineWidth, 1.0f); + lineMetrics.underlineWidth = std::max(lineMetrics.underlineWidth, 1.0f); + lineMetrics.strikethroughWidth = std::max(lineMetrics.strikethroughWidth, 1.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. + lineMetrics.underlineOffset = fullPixelAscent - lineMetrics.underlineOffset; + lineMetrics.strikethroughOffset = fullPixelAscent - lineMetrics.strikethroughOffset; + + // 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::round(fontSize * 0.05f); + + // 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 = lineSpacing.height - lineMetrics.underlineWidth; + lineMetrics.underlineOffset2 = std::min(lineMetrics.underlineOffset2, maxUnderlineOffset); + + // 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; + } + + // We also add half the stroke width to the offsets, since the line + // coordinates designate the center of the line. + lineMetrics.underlineOffset += lineMetrics.underlineWidth / 2.0f; + lineMetrics.underlineOffset2 += lineMetrics.underlineWidth / 2.0f; + lineMetrics.strikethroughOffset += lineMetrics.strikethroughWidth / 2.0f; + + _lineMetrics = lineMetrics; + + _glyphCell = actual.GetSize(); + + // Calculate and cache the box effect for the base font. Scale is 1.0f because the base font is exactly the scale we want already. + RETURN_IF_FAILED(s_CalculateBoxEffect(DefaultTextFormat().Get(), _glyphCell.width(), DefaultFontFace().Get(), 1.0f, &_boxDrawingEffect)); + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - Calculates the box drawing scale/translate matrix values to fit a box glyph into the cell as perfectly as possible. +// Arguments: +// - format - Text format used to determine line spacing (height including ascent & descent) as calculated from the base font. +// - widthPixels - The pixel width of the available cell. +// - face - The font face that is currently being used, may differ from the base font from the layout. +// - fontScale - if the given font face is going to be scaled versus the format, we need to know so we can compensate for that. pass 1.0f for no scaling. +// - effect - Receives the effect to apply to box drawing characters. If no effect is received, special treatment isn't required. +// Return Value: +// - S_OK, GSL/WIL errors, DirectWrite errors, or math errors. +[[nodiscard]] HRESULT STDMETHODCALLTYPE DxFontRenderData::s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept +try +{ + // Check for bad in parameters. + RETURN_HR_IF(E_INVALIDARG, !format); + RETURN_HR_IF(E_INVALIDARG, !face); + + // Check the out parameter and fill it up with null. + RETURN_HR_IF(E_INVALIDARG, !effect); + *effect = nullptr; + + // The format is based around the main font that was specified by the user. + // We need to know its size as well as the final spacing that was calculated around + // it when it was first selected to get an idea of how large the bounding box is. + const auto fontSize = format->GetFontSize(); + + DWRITE_LINE_SPACING_METHOD spacingMethod; + float lineSpacing; // total height of the cells + float baseline; // vertical position counted down from the top where the characters "sit" + RETURN_IF_FAILED(format->GetLineSpacing(&spacingMethod, &lineSpacing, &baseline)); + + const float ascentPixels = baseline; + const float descentPixels = lineSpacing - baseline; + + // We need this for the designUnitsPerEm which will be required to move back and forth between + // Design Units and Pixels. I'll elaborate below. + DWRITE_FONT_METRICS1 fontMetrics; + face->GetMetrics(&fontMetrics); + + // If we had font fallback occur, the size of the font given to us (IDWriteFontFace1) can be different + // than the font size used for the original format (IDWriteTextFormat). + const auto scaledFontSize = fontScale * fontSize; + + // This is Unicode FULL BLOCK U+2588. + // We presume that FULL BLOCK should be filling its entire cell in all directions so it should provide a good basis + // in knowing exactly where to touch every single edge. + // We're also presuming that the other box/line drawing glyphs were authored in this font to perfectly inscribe + // inside of FULL BLOCK, with the same left/top/right/bottom bearings so they would look great when drawn adjacent. + const UINT32 blockCodepoint = L'\x2588'; + + // Get the index of the block out of the font. + UINT16 glyphIndex; + RETURN_IF_FAILED(face->GetGlyphIndicesW(&blockCodepoint, 1, &glyphIndex)); + + // If it was 0, it wasn't found in the font. We're going to try again with + // Unicode BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL U+253C which should be touching + // all the edges of the possible rectangle, much like a full block should. + if (glyphIndex == 0) + { + const UINT32 alternateCp = L'\x253C'; + RETURN_IF_FAILED(face->GetGlyphIndicesW(&alternateCp, 1, &glyphIndex)); + } + + // If we still didn't find the glyph index, we haven't implemented any further logic to figure out the box dimensions. + // So we're just going to leave successfully as is and apply no scaling factor. It might look not-right, but it won't + // stop the rendering pipeline. + RETURN_HR_IF(S_FALSE, glyphIndex == 0); + + // Get the metrics of the given glyph, which we're going to treat as the outline box in which all line/block drawing + // glyphs will be inscribed within, perfectly touching each edge as to align when two cells meet. + DWRITE_GLYPH_METRICS boxMetrics = { 0 }; + RETURN_IF_FAILED(face->GetDesignGlyphMetrics(&glyphIndex, 1, &boxMetrics)); + + // NOTE: All metrics we receive from DWRITE are going to be in "design units" which are a somewhat agnostic + // way of describing proportions. + // Converting back and forth between real pixels and design units is possible using + // any font's specific fontSize and the designUnitsPerEm FONT_METRIC value. + // + // Here's what to know about the boxMetrics: + // + // + // + // topLeft --> +--------------------------------+ --- + // | ^ | | + // | | topSide | | + // | | Bearing | | + // | v | | + // | +-----------------+ | | + // | | | | | + // | | | | | a + // | | | | | d + // | | | | | v + // +<---->+ | | | a + // | | | | | n + // | left | | | | c + // | Side | | | | e + // | Bea- | | | | H + // | ring | | right | | e + // vertical | | | Side | | i + // OriginY --> x | | Bea- | | g + // | | | ring | | h + // | | | | | t + // | | +<----->+ | + // | +-----------------+ | | + // | ^ | | + // | bottomSide | | | + // | Bearing | | | + // | v | | + // +--------------------------------+ --- + // + // + // | | + // +--------------------------------+ + // | advanceWidth | + // + // + // NOTE: The bearings can be negative, in which case it is specifying that the glyphs overhang the box + // as defined by the advanceHeight/width. + // See also: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics + + // The scale is a multiplier and the translation is addition. So *1 and +0 will mean nothing happens. + const float defaultBoxVerticalScaleFactor = 1.0f; + float boxVerticalScaleFactor = defaultBoxVerticalScaleFactor; + const float defaultBoxVerticalTranslation = 0.0f; + float boxVerticalTranslation = defaultBoxVerticalTranslation; + { + // First, find the dimensions of the glyph representing our fully filled box. + + // Ascent is how far up from the baseline we'll draw. + // verticalOriginY is the measure from the topLeft corner of the bounding box down to where + // the glyph's version of the baseline is. + // topSideBearing is how much "gap space" is left between that topLeft and where the glyph + // starts drawing. Subtract the gap space to find how far is drawn upward from baseline. + const auto boxAscentDesignUnits = boxMetrics.verticalOriginY - boxMetrics.topSideBearing; + + // Descent is how far down from the baseline we'll draw. + // advanceHeight is the total height of the drawn bounding box. + // verticalOriginY is how much was given to the ascent, so subtract that out. + // What remains is then the descent value. Remove the + // bottomSideBearing as the "gap space" on the bottom to find how far is drawn downward from baseline. + const auto boxDescentDesignUnits = boxMetrics.advanceHeight - boxMetrics.verticalOriginY - boxMetrics.bottomSideBearing; + + // The height, then, of the entire box is just the sum of the ascent above the baseline and the descent below. + const auto boxHeightDesignUnits = boxAscentDesignUnits + boxDescentDesignUnits; + + // Second, find the dimensions of the cell we're going to attempt to fit within. + // We know about the exact ascent/descent units in pixels as calculated when we chose a font and + // adjusted the ascent/descent for a nice perfect baseline and integer total height. + // All we need to do is adapt it into Design Units so it meshes nicely with the Design Units above. + // Use the formula: Pixels * Design Units Per Em / Font Size = Design Units + const auto cellAscentDesignUnits = ascentPixels * fontMetrics.designUnitsPerEm / scaledFontSize; + const auto cellDescentDesignUnits = descentPixels * fontMetrics.designUnitsPerEm / scaledFontSize; + const auto cellHeightDesignUnits = cellAscentDesignUnits + cellDescentDesignUnits; + + // OK, now do a few checks. If the drawn box touches the top and bottom of the cell + // and the box is overall tall enough, then we'll not bother adjusting. + // We will presume the font author has set things as they wish them to be. + const auto boxTouchesCellTop = boxAscentDesignUnits >= cellAscentDesignUnits; + const auto boxTouchesCellBottom = boxDescentDesignUnits >= cellDescentDesignUnits; + const auto boxIsTallEnoughForCell = boxHeightDesignUnits >= cellHeightDesignUnits; + + // If not... + if (!(boxTouchesCellTop && boxTouchesCellBottom && boxIsTallEnoughForCell)) + { + // Find a scaling factor that will make the total height drawn of this box + // perfectly fit the same number of design units as the cell. + // Since scale factor is a multiplier, it doesn't matter that this is design units. + // The fraction between the two heights in pixels should be exactly the same + // (which is what will matter when we go to actually render it... the pixels that is.) + // Don't scale below 1.0. If it'd shrink, just center it at the prescribed scale. + boxVerticalScaleFactor = std::max(cellHeightDesignUnits / boxHeightDesignUnits, 1.0f); + + // The box as scaled might be hanging over the top or bottom of the cell (or both). + // We find out the amount of overhang/underhang on both the top and the bottom. + const auto extraAscent = boxAscentDesignUnits * boxVerticalScaleFactor - cellAscentDesignUnits; + const auto extraDescent = boxDescentDesignUnits * boxVerticalScaleFactor - cellDescentDesignUnits; + + // This took a bit of time and effort and it's difficult to put into words, but here goes. + // We want the average of the two magnitudes to find out how much to "take" from one and "give" + // to the other such that both are equal. We presume the glyphs are designed to be drawn + // centered in their box vertically to look good. + // The ordering around subtraction is required to ensure that the direction is correct with a negative + // translation moving up (taking excess descent and adding to ascent) and positive is the opposite. + const auto boxVerticalTranslationDesignUnits = (extraAscent - extraDescent) / 2; + + // The translation is just a raw movement of pixels up or down. Since we were working in Design Units, + // we need to run the opposite algorithm shown above to go from Design Units to Pixels. + boxVerticalTranslation = boxVerticalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm; + } + } + + // The horizontal adjustments follow the exact same logic as the vertical ones. + const float defaultBoxHorizontalScaleFactor = 1.0f; + float boxHorizontalScaleFactor = defaultBoxHorizontalScaleFactor; + const float defaultBoxHorizontalTranslation = 0.0f; + float boxHorizontalTranslation = defaultBoxHorizontalTranslation; + { + // This is the only difference. We don't have a horizontalOriginX from the metrics. + // However, https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics says + // the X coordinate is specified by half the advanceWidth to the right of the horizontalOrigin. + // So we'll use that as the "center" and apply it the role that verticalOriginY had above. + + const auto boxCenterDesignUnits = boxMetrics.advanceWidth / 2; + const auto boxLeftDesignUnits = boxCenterDesignUnits - boxMetrics.leftSideBearing; + const auto boxRightDesignUnits = boxMetrics.advanceWidth - boxMetrics.rightSideBearing - boxCenterDesignUnits; + const auto boxWidthDesignUnits = boxLeftDesignUnits + boxRightDesignUnits; + + const auto cellWidthDesignUnits = widthPixels * fontMetrics.designUnitsPerEm / scaledFontSize; + const auto cellLeftDesignUnits = cellWidthDesignUnits / 2; + const auto cellRightDesignUnits = cellLeftDesignUnits; + + const auto boxTouchesCellLeft = boxLeftDesignUnits >= cellLeftDesignUnits; + const auto boxTouchesCellRight = boxRightDesignUnits >= cellRightDesignUnits; + const auto boxIsWideEnoughForCell = boxWidthDesignUnits >= cellWidthDesignUnits; + + if (!(boxTouchesCellLeft && boxTouchesCellRight && boxIsWideEnoughForCell)) + { + boxHorizontalScaleFactor = std::max(cellWidthDesignUnits / boxWidthDesignUnits, 1.0f); + const auto extraLeft = boxLeftDesignUnits * boxHorizontalScaleFactor - cellLeftDesignUnits; + const auto extraRight = boxRightDesignUnits * boxHorizontalScaleFactor - cellRightDesignUnits; + + const auto boxHorizontalTranslationDesignUnits = (extraLeft - extraRight) / 2; + + boxHorizontalTranslation = boxHorizontalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm; + } + } + + // If we set anything, make a drawing effect. Otherwise, there isn't one. + if (defaultBoxVerticalScaleFactor != boxVerticalScaleFactor || + defaultBoxVerticalTranslation != boxVerticalTranslation || + defaultBoxHorizontalScaleFactor != boxHorizontalScaleFactor || + defaultBoxHorizontalTranslation != boxHorizontalTranslation) + { + // OK, make the object that will represent our effect, stuff the metrics into it, and return it. + RETURN_IF_FAILED(WRL::MakeAndInitialize(effect, boxVerticalScaleFactor, boxVerticalTranslation, boxHorizontalScaleFactor, boxHorizontalTranslation)); + } + + return S_OK; +} +CATCH_RETURN() + +// Routine Description: +// - Attempts to locate the font given, but then begins falling back if we cannot find it. +// - We'll try to fall back to Consolas with the given weight/stretch/style first, +// then try Consolas again with normal weight/stretch/style, +// and if nothing works, then we'll throw an error. +// Arguments: +// - familyName - The font name we should be looking for +// - weight - The weight (bold, light, etc.) +// - stretch - The stretch of the font is the spacing between each letter +// - style - Normal, italic, etc. +// Return Value: +// - Smart pointer holding interface reference for queryable font data. +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::_ResolveFontFaceWithFallback(std::wstring& familyName, + DWRITE_FONT_WEIGHT& weight, + DWRITE_FONT_STRETCH& stretch, + DWRITE_FONT_STYLE& style, + std::wstring& localeName) const +{ + auto face = _FindFontFace(familyName, weight, stretch, style, localeName); + + if (!face) + { + for (const auto fallbackFace : FALLBACK_FONT_FACES) + { + familyName = fallbackFace; + face = _FindFontFace(familyName, weight, stretch, style, localeName); + + if (face) + { + break; + } + + familyName = fallbackFace; + weight = DWRITE_FONT_WEIGHT_NORMAL; + stretch = DWRITE_FONT_STRETCH_NORMAL; + style = DWRITE_FONT_STYLE_NORMAL; + face = _FindFontFace(familyName, weight, stretch, style, localeName); + + if (face) + { + break; + } + } + } + + THROW_HR_IF_NULL(E_FAIL, face); + + return face; +} + +// Routine Description: +// - Locates a suitable font face from the given information +// Arguments: +// - familyName - The font name we should be looking for +// - weight - The weight (bold, light, etc.) +// - stretch - The stretch of the font is the spacing between each letter +// - style - Normal, italic, etc. +// Return Value: +// - Smart pointer holding interface reference for queryable font data. +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::_FindFontFace(std::wstring& familyName, + DWRITE_FONT_WEIGHT& weight, + DWRITE_FONT_STRETCH& stretch, + DWRITE_FONT_STYLE& style, + std::wstring& localeName) const +{ + Microsoft::WRL::ComPtr fontFace; + + Microsoft::WRL::ComPtr fontCollection; + THROW_IF_FAILED(_dwriteFactory->GetSystemFontCollection(&fontCollection, false)); + + UINT32 familyIndex; + BOOL familyExists; + THROW_IF_FAILED(fontCollection->FindFamilyName(familyName.data(), &familyIndex, &familyExists)); + + if (familyExists) + { + Microsoft::WRL::ComPtr fontFamily; + THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily)); + + Microsoft::WRL::ComPtr font; + THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(weight, stretch, style, &font)); + + Microsoft::WRL::ComPtr fontFace0; + THROW_IF_FAILED(font->CreateFontFace(&fontFace0)); + + THROW_IF_FAILED(fontFace0.As(&fontFace)); + + // Retrieve metrics in case the font we created was different than what was requested. + weight = font->GetWeight(); + stretch = font->GetStretch(); + style = font->GetStyle(); + + // Dig the family name out at the end to return it. + familyName = _GetFontFamilyName(fontFamily.Get(), localeName); + } + + return fontFace; +} + +// Routine Description: +// - Retrieves the font family name out of the given object in the given locale. +// - If we can't find a valid name for the given locale, we'll fallback and report it back. +// Arguments: +// - fontFamily - DirectWrite font family object +// - localeName - The locale in which the name should be retrieved. +// - If fallback occurred, this is updated to what we retrieved instead. +// Return Value: +// - Localized string name of the font family +[[nodiscard]] std::wstring DxFontRenderData::_GetFontFamilyName(gsl::not_null const fontFamily, + std::wstring& localeName) const +{ + // See: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontcollection + Microsoft::WRL::ComPtr familyNames; + THROW_IF_FAILED(fontFamily->GetFamilyNames(&familyNames)); + + // First we have to find the right family name for the locale. We're going to bias toward what the caller + // requested, but fallback if we need to and reply with the locale we ended up choosing. + UINT32 index = 0; + BOOL exists = false; + + // This returns S_OK whether or not it finds a locale name. Check exists field instead. + // If it returns an error, it's a real problem, not an absence of this locale name. + // https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-findlocalename + THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists)); + + // If we tried and it still doesn't exist, try with the fallback locale. + if (!exists) + { + localeName = FALLBACK_LOCALE; + THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists)); + } + + // If it still doesn't exist, we're going to try index 0. + if (!exists) + { + index = 0; + + // Get the locale name out so at least the caller knows what locale this name goes with. + UINT32 length = 0; + THROW_IF_FAILED(familyNames->GetLocaleNameLength(index, &length)); + localeName.resize(length); + + // https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getlocalenamelength + // https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getlocalename + // GetLocaleNameLength does not include space for null terminator, but GetLocaleName needs it so add one. + THROW_IF_FAILED(familyNames->GetLocaleName(index, localeName.data(), length + 1)); + } + + // OK, now that we've decided which family name and the locale that it's in... let's go get it. + UINT32 length = 0; + THROW_IF_FAILED(familyNames->GetStringLength(index, &length)); + + // Make our output buffer and resize it so it is allocated. + std::wstring retVal; + retVal.resize(length); + + // FINALLY, go fetch the string name. + // https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getstringlength + // https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getstring + // Once again, GetStringLength is without the null, but GetString needs the null. So add one. + THROW_IF_FAILED(familyNames->GetString(index, retVal.data(), length + 1)); + + // and return it. + return retVal; +} + +[[nodiscard]] std::wstring DxFontRenderData::_GetUserLocaleName() +{ + if (_userLocaleName.empty()) + { + std::array localeName; + + const auto returnCode = GetUserDefaultLocaleName(localeName.data(), gsl::narrow(localeName.size())); + if (returnCode) + { + _userLocaleName = { localeName.data() }; + } + else + { + _userLocaleName = { FALLBACK_LOCALE.data(), FALLBACK_LOCALE.size() }; + } + } + + return _userLocaleName; +} diff --git a/src/renderer/dx/DxFontRenderData.h b/src/renderer/dx/DxFontRenderData.h new file mode 100644 index 00000000000..403d323e565 --- /dev/null +++ b/src/renderer/dx/DxFontRenderData.h @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "../../renderer/inc/FontInfoDesired.hpp" +#include "BoxDrawingEffect.h" + +#include +#include +#include +#include + +#include + +namespace Microsoft::Console::Render +{ + class DxFontRenderData + { + public: + struct LineMetrics + { + float gridlineWidth; + float underlineOffset; + float underlineOffset2; + float underlineWidth; + float strikethroughOffset; + float strikethroughWidth; + }; + + DxFontRenderData(::Microsoft::WRL::ComPtr dwriteFactory) noexcept; + + // DirectWrite text analyzer from the factory + [[nodiscard]] Microsoft::WRL::ComPtr Analyzer() noexcept; + + [[nodiscard]] Microsoft::WRL::ComPtr SystemFontFallback(); + + [[nodiscard]] til::size GlyphCell() noexcept; + [[nodiscard]] LineMetrics GetLineMetrics() noexcept; + + // The DirectWrite format object representing the size and other text properties to be applied (by default) + [[nodiscard]] Microsoft::WRL::ComPtr DefaultTextFormat() noexcept; + + // The DirectWrite font face to use while calculating layout (by default) + [[nodiscard]] Microsoft::WRL::ComPtr DefaultFontFace() noexcept; + + // Box drawing scaling effects that are cached for the base font across layouts + [[nodiscard]] Microsoft::WRL::ComPtr DefaultBoxDrawingEffect() noexcept; + + // The italic variant of the format object representing the size and other text properties for italic text + [[nodiscard]] Microsoft::WRL::ComPtr ItalicTextFormat() noexcept; + + // The italic variant of the font face to use while calculating layout for italic text + [[nodiscard]] Microsoft::WRL::ComPtr ItalicFontFace() noexcept; + + [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& desired, FontInfo& fiFontInfo, const int dpi) noexcept; + + [[nodiscard]] static HRESULT STDMETHODCALLTYPE s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept; + + private: + [[nodiscard]] ::Microsoft::WRL::ComPtr _ResolveFontFaceWithFallback(std::wstring& familyName, + DWRITE_FONT_WEIGHT& weight, + DWRITE_FONT_STRETCH& stretch, + DWRITE_FONT_STYLE& style, + std::wstring& localeName) const; + + [[nodiscard]] ::Microsoft::WRL::ComPtr _FindFontFace(std::wstring& familyName, + DWRITE_FONT_WEIGHT& weight, + DWRITE_FONT_STRETCH& stretch, + DWRITE_FONT_STYLE& style, + std::wstring& localeName) const; + + [[nodiscard]] std::wstring _GetFontFamilyName(gsl::not_null const fontFamily, + std::wstring& localeName) const; + + // A locale that can be used on construction of assorted DX objects that want to know one. + [[nodiscard]] std::wstring _GetUserLocaleName(); + + ::Microsoft::WRL::ComPtr _dwriteFactory; + + ::Microsoft::WRL::ComPtr _dwriteTextAnalyzer; + ::Microsoft::WRL::ComPtr _dwriteTextFormat; + ::Microsoft::WRL::ComPtr _dwriteTextFormatItalic; + ::Microsoft::WRL::ComPtr _dwriteFontFace; + ::Microsoft::WRL::ComPtr _dwriteFontFaceItalic; + + ::Microsoft::WRL::ComPtr _boxDrawingEffect; + + ::Microsoft::WRL::ComPtr _systemFontFallback; + std::wstring _userLocaleName; + + til::size _glyphCell; + + LineMetrics _lineMetrics; + }; +} diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index b124c621f3e..0ffbc1584ec 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -51,10 +51,6 @@ D3D11_INPUT_ELEMENT_DESC _shaderInputLayout[] = { #pragma hdrstop -static constexpr float POINTS_PER_INCH = 72.0f; -static constexpr std::wstring_view FALLBACK_FONT_FACES[] = { L"Consolas", L"Lucida Console", L"Courier New" }; -static constexpr std::wstring_view FALLBACK_LOCALE = L"en-us"; - using namespace Microsoft::Console::Render; using namespace Microsoft::Console::Types; @@ -82,8 +78,6 @@ DxEngine::DxEngine() : _foregroundColor{ 0 }, _backgroundColor{ 0 }, _selectionBackground{}, - _glyphCell{}, - _boxDrawingEffect{}, _haveDeviceResources{ false }, _swapChainDesc{ 0 }, _swapChainFrameLatencyWaitableObject{ INVALID_HANDLE_VALUE }, @@ -121,6 +115,8 @@ DxEngine::DxEngine() : // Initialize our default selection color to DEFAULT_FOREGROUND, but make // sure to set to to a D2D1::ColorF SetSelectionBackground(DEFAULT_FOREGROUND); + + _fontRenderData = std::make_unique(_dwriteFactory); } // Routine Description: @@ -910,9 +906,9 @@ try { return _dwriteFactory->CreateTextLayout(string, gsl::narrow(stringLength), - _dwriteTextFormat.Get(), + _fontRenderData->DefaultTextFormat().Get(), _displaySizePixels.width(), - _glyphCell.height() != 0 ? _glyphCell.height() : _displaySizePixels.height(), + _fontRenderData->GlyphCell().height() != 0 ? _fontRenderData->GlyphCell().height() : _displaySizePixels.height(), ppTextLayout); } CATCH_RETURN() @@ -936,7 +932,7 @@ try { _sizeTarget = Pixels; - _invalidMap.resize(_sizeTarget / _glyphCell, true); + _invalidMap.resize(_sizeTarget / _fontRenderData->GlyphCell(), true); return S_OK; } CATCH_RETURN(); @@ -1080,7 +1076,7 @@ try { // Dirty client is in pixels. Use divide specialization against glyph factor to make conversion // to cells. - _InvalidateRectangle(til::rectangle{ *prcDirtyClient }.scale_down(_glyphCell)); + _InvalidateRectangle(til::rectangle{ *prcDirtyClient }.scale_down(_fontRenderData->GlyphCell())); } return S_OK; @@ -1303,7 +1299,7 @@ try { // Get the baseline for this font as that's where we draw from DWRITE_LINE_SPACING spacing; - RETURN_IF_FAILED(_dwriteTextFormat->GetLineSpacing(&spacing.method, &spacing.height, &spacing.baseline)); + RETURN_IF_FAILED(_fontRenderData->DefaultTextFormat()->GetLineSpacing(&spacing.method, &spacing.height, &spacing.baseline)); // Assemble the drawing context information _drawingContext = std::make_unique(_d2dDeviceContext.Get(), @@ -1312,7 +1308,7 @@ try _ShouldForceGrayscaleAA(), _dwriteFactory.Get(), spacing, - _glyphCell, + _fontRenderData->GlyphCell(), _d2dDeviceContext->GetSize(), std::nullopt, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT); @@ -1354,14 +1350,14 @@ try // Scale all dirty rectangles into pixels std::transform(_presentDirty.begin(), _presentDirty.end(), _presentDirty.begin(), [&](til::rectangle rc) { - return rc.scale_up(_glyphCell); + return rc.scale_up(_fontRenderData->GlyphCell()); }); // Invalid scroll is in characters, convert it to pixels. - const auto scrollPixels = (_invalidScroll * _glyphCell); + const auto scrollPixels = (_invalidScroll * _fontRenderData->GlyphCell()); // The scroll rect is the entire field of cells, but in pixels. - til::rectangle scrollArea{ _invalidMap.size() * _glyphCell }; + til::rectangle scrollArea{ _invalidMap.size() * _fontRenderData->GlyphCell() }; // Reduce the size of the rectangle by the scroll. scrollArea -= til::size{} - scrollPixels; @@ -1609,7 +1605,7 @@ try // Runs are counts of cells. // Use a transform by the size of one cell to convert cells-to-pixels // as we clear. - _d2dDeviceContext->SetTransform(D2D1::Matrix3x2F::Scale(_glyphCell)); + _d2dDeviceContext->SetTransform(D2D1::Matrix3x2F::Scale(_fontRenderData->GlyphCell())); for (const auto& rect : _invalidMap.runs()) { // Use aliased. @@ -1643,7 +1639,7 @@ CATCH_RETURN() try { // Calculate positioning of our origin. - const D2D1_POINT_2F origin = til::point{ coord } * _glyphCell; + const D2D1_POINT_2F origin = til::point{ coord } * _fontRenderData->GlyphCell(); // Create the text layout RETURN_IF_FAILED(_customLayout->Reset()); @@ -1677,7 +1673,7 @@ try _d2dBrushForeground->SetColor(_ColorFFromColorRef(color)); - const D2D1_SIZE_F font = _glyphCell; + const D2D1_SIZE_F font = _fontRenderData->GlyphCell(); const D2D_POINT_2F target = { coordTarget.X * font.width, coordTarget.Y * font.height }; const auto fullRunWidth = font.width * gsl::narrow_cast(cchLine); @@ -1692,10 +1688,10 @@ try // 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. - + const DxFontRenderData::LineMetrics lineMetrics = _fontRenderData->GetLineMetrics(); if (WI_IsAnyFlagSet(lines, (GridLines::Left | GridLines::Right))) { - const auto halfGridlineWidth = _lineMetrics.gridlineWidth / 2.0f; + const auto halfGridlineWidth = lineMetrics.gridlineWidth / 2.0f; const auto startY = target.y + halfGridlineWidth; const auto endY = target.y + font.height - halfGridlineWidth; @@ -1704,7 +1700,7 @@ try auto x = target.x + halfGridlineWidth; for (size_t i = 0; i < cchLine; i++, x += font.width) { - DrawLine(x, startY, x, endY, _lineMetrics.gridlineWidth); + DrawLine(x, startY, x, endY, lineMetrics.gridlineWidth); } } @@ -1713,27 +1709,27 @@ try auto x = target.x + font.width - halfGridlineWidth; for (size_t i = 0; i < cchLine; i++, x += font.width) { - DrawLine(x, startY, x, endY, _lineMetrics.gridlineWidth); + DrawLine(x, startY, x, endY, lineMetrics.gridlineWidth); } } } if (WI_IsAnyFlagSet(lines, GridLines::Top | GridLines::Bottom)) { - const auto halfGridlineWidth = _lineMetrics.gridlineWidth / 2.0f; + const auto halfGridlineWidth = lineMetrics.gridlineWidth / 2.0f; const auto startX = target.x + halfGridlineWidth; const auto endX = target.x + fullRunWidth - halfGridlineWidth; if (WI_IsFlagSet(lines, GridLines::Top)) { const auto y = target.y + halfGridlineWidth; - DrawLine(startX, y, endX, y, _lineMetrics.gridlineWidth); + DrawLine(startX, y, endX, y, lineMetrics.gridlineWidth); } if (WI_IsFlagSet(lines, GridLines::Bottom)) { const auto y = target.y + font.height - halfGridlineWidth; - DrawLine(startX, y, endX, y, _lineMetrics.gridlineWidth); + DrawLine(startX, y, endX, y, lineMetrics.gridlineWidth); } } @@ -1742,37 +1738,37 @@ try if (WI_IsAnyFlagSet(lines, GridLines::Underline | GridLines::DoubleUnderline | GridLines::HyperlinkUnderline)) { - const auto halfUnderlineWidth = _lineMetrics.underlineWidth / 2.0f; + const auto halfUnderlineWidth = lineMetrics.underlineWidth / 2.0f; const auto startX = target.x + halfUnderlineWidth; const auto endX = target.x + fullRunWidth - halfUnderlineWidth; - const auto y = target.y + _lineMetrics.underlineOffset; + const auto y = target.y + lineMetrics.underlineOffset; if (WI_IsFlagSet(lines, GridLines::Underline)) { - DrawLine(startX, y, endX, y, _lineMetrics.underlineWidth); + DrawLine(startX, y, endX, y, lineMetrics.underlineWidth); } if (WI_IsFlagSet(lines, GridLines::HyperlinkUnderline)) { - DrawHyperlinkLine(startX, y, endX, y, _lineMetrics.underlineWidth); + DrawHyperlinkLine(startX, y, endX, y, lineMetrics.underlineWidth); } if (WI_IsFlagSet(lines, GridLines::DoubleUnderline)) { - DrawLine(startX, y, endX, y, _lineMetrics.underlineWidth); - const auto y2 = target.y + _lineMetrics.underlineOffset2; - DrawLine(startX, y2, endX, y2, _lineMetrics.underlineWidth); + DrawLine(startX, y, endX, y, lineMetrics.underlineWidth); + const auto y2 = target.y + lineMetrics.underlineOffset2; + DrawLine(startX, y2, endX, y2, lineMetrics.underlineWidth); } } if (WI_IsFlagSet(lines, GridLines::Strikethrough)) { - const auto halfStrikethroughWidth = _lineMetrics.strikethroughWidth / 2.0f; + 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; + const auto y = target.y + lineMetrics.strikethroughOffset; - DrawLine(startX, y, endX, y, _lineMetrics.strikethroughWidth); + DrawLine(startX, y, endX, y, lineMetrics.strikethroughWidth); } return S_OK; @@ -1796,7 +1792,7 @@ try _d2dBrushForeground->SetColor(_selectionBackground); const auto resetColorOnExit = wil::scope_exit([&]() noexcept { _d2dBrushForeground->SetColor(existingColor); }); - const D2D1_RECT_F draw = til::rectangle{ Viewport::FromExclusive(rect).ToInclusive() }.scale_up(_glyphCell); + const D2D1_RECT_F draw = til::rectangle{ Viewport::FromExclusive(rect).ToInclusive() }.scale_up(_fontRenderData->GlyphCell()); _d2dDeviceContext->FillRectangle(draw, _d2dBrushForeground.Get()); @@ -1956,30 +1952,10 @@ CATCH_RETURN() [[nodiscard]] HRESULT DxEngine::UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo) noexcept try { - RETURN_IF_FAILED(_GetProposedFont(pfiFontInfoDesired, - fiFontInfo, - _dpi, - _dwriteTextFormat, - _dwriteTextFormatItalic, - _dwriteTextAnalyzer, - _dwriteFontFace, - _dwriteFontFaceItalic, - _lineMetrics)); - - _glyphCell = fiFontInfo.GetSize(); - - // Calculate and cache the box effect for the base font. Scale is 1.0f because the base font is exactly the scale we want already. - RETURN_IF_FAILED(CustomTextLayout::s_CalculateBoxEffect(_dwriteTextFormat.Get(), _glyphCell.width(), _dwriteFontFace.Get(), 1.0f, &_boxDrawingEffect)); + RETURN_IF_FAILED(_fontRenderData->UpdateFont(pfiFontInfoDesired, fiFontInfo, _dpi)); // Prepare the text layout. - _customLayout = WRL::Make(_dwriteFactory.Get(), - _dwriteTextAnalyzer.Get(), - _dwriteTextFormat.Get(), - _dwriteTextFormatItalic.Get(), - _dwriteFontFace.Get(), - _dwriteFontFaceItalic.Get(), - _glyphCell.width(), - _boxDrawingEffect.Get()); + _customLayout = WRL::Make(_fontRenderData.get()); return S_OK; } @@ -1987,16 +1963,16 @@ CATCH_RETURN(); [[nodiscard]] Viewport DxEngine::GetViewportInCharacters(const Viewport& viewInPixels) noexcept { - const short widthInChars = base::saturated_cast(viewInPixels.Width() / _glyphCell.width()); - const short heightInChars = base::saturated_cast(viewInPixels.Height() / _glyphCell.height()); + const short widthInChars = base::saturated_cast(viewInPixels.Width() / _fontRenderData->GlyphCell().width()); + const short heightInChars = base::saturated_cast(viewInPixels.Height() / _fontRenderData->GlyphCell().height()); return Viewport::FromDimensions(viewInPixels.Origin(), { widthInChars, heightInChars }); } [[nodiscard]] Viewport DxEngine::GetViewportInPixels(const Viewport& viewInCharacters) noexcept { - const short widthInPixels = base::saturated_cast(viewInCharacters.Width() * _glyphCell.width()); - const short heightInPixels = base::saturated_cast(viewInCharacters.Height() * _glyphCell.height()); + const short widthInPixels = base::saturated_cast(viewInCharacters.Width() * _fontRenderData->GlyphCell().width()); + const short heightInPixels = base::saturated_cast(viewInCharacters.Height() * _fontRenderData->GlyphCell().height()); return Viewport::FromDimensions(viewInCharacters.Origin(), { widthInPixels, heightInPixels }); } @@ -2058,22 +2034,8 @@ float DxEngine::GetScaling() const noexcept FontInfo& pfiFontInfo, int const iDpi) noexcept { - Microsoft::WRL::ComPtr format; - Microsoft::WRL::ComPtr formatItalic; - Microsoft::WRL::ComPtr analyzer; - Microsoft::WRL::ComPtr face; - Microsoft::WRL::ComPtr faceItalic; - LineMetrics lineMetrics; - - return _GetProposedFont(pfiFontInfoDesired, - pfiFontInfo, - iDpi, - format, - formatItalic, - analyzer, - face, - faceItalic, - lineMetrics); + DxFontRenderData fontRenderData(_dwriteFactory); + return fontRenderData.UpdateFont(pfiFontInfoDesired, pfiFontInfo, iDpi); } // Routine Description: @@ -2099,7 +2061,7 @@ CATCH_RETURN(); [[nodiscard]] HRESULT DxEngine::GetFontSize(_Out_ COORD* const pFontSize) noexcept try { - *pFontSize = _glyphCell; + *pFontSize = _fontRenderData->GlyphCell(); return S_OK; } CATCH_RETURN(); @@ -2145,447 +2107,6 @@ CATCH_RETURN(); return S_FALSE; } -// Routine Description: -// - Attempts to locate the font given, but then begins falling back if we cannot find it. -// - We'll try to fall back to Consolas with the given weight/stretch/style first, -// then try Consolas again with normal weight/stretch/style, -// and if nothing works, then we'll throw an error. -// Arguments: -// - familyName - The font name we should be looking for -// - weight - The weight (bold, light, etc.) -// - stretch - The stretch of the font is the spacing between each letter -// - style - Normal, italic, etc. -// Return Value: -// - Smart pointer holding interface reference for queryable font data. -[[nodiscard]] Microsoft::WRL::ComPtr DxEngine::_ResolveFontFaceWithFallback(std::wstring& familyName, - DWRITE_FONT_WEIGHT& weight, - DWRITE_FONT_STRETCH& stretch, - DWRITE_FONT_STYLE& style, - std::wstring& localeName) const -{ - auto face = _FindFontFace(familyName, weight, stretch, style, localeName); - - if (!face) - { - for (const auto fallbackFace : FALLBACK_FONT_FACES) - { - familyName = fallbackFace; - face = _FindFontFace(familyName, weight, stretch, style, localeName); - - if (face) - { - break; - } - - familyName = fallbackFace; - weight = DWRITE_FONT_WEIGHT_NORMAL; - stretch = DWRITE_FONT_STRETCH_NORMAL; - style = DWRITE_FONT_STYLE_NORMAL; - face = _FindFontFace(familyName, weight, stretch, style, localeName); - - if (face) - { - break; - } - } - } - - THROW_HR_IF_NULL(E_FAIL, face); - - return face; -} - -// Routine Description: -// - Locates a suitable font face from the given information -// Arguments: -// - familyName - The font name we should be looking for -// - weight - The weight (bold, light, etc.) -// - stretch - The stretch of the font is the spacing between each letter -// - style - Normal, italic, etc. -// Return Value: -// - Smart pointer holding interface reference for queryable font data. -[[nodiscard]] Microsoft::WRL::ComPtr DxEngine::_FindFontFace(std::wstring& familyName, - DWRITE_FONT_WEIGHT& weight, - DWRITE_FONT_STRETCH& stretch, - DWRITE_FONT_STYLE& style, - std::wstring& localeName) const -{ - Microsoft::WRL::ComPtr fontFace; - - Microsoft::WRL::ComPtr fontCollection; - THROW_IF_FAILED(_dwriteFactory->GetSystemFontCollection(&fontCollection, false)); - - UINT32 familyIndex; - BOOL familyExists; - THROW_IF_FAILED(fontCollection->FindFamilyName(familyName.data(), &familyIndex, &familyExists)); - - if (familyExists) - { - Microsoft::WRL::ComPtr fontFamily; - THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily)); - - Microsoft::WRL::ComPtr font; - THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(weight, stretch, style, &font)); - - Microsoft::WRL::ComPtr fontFace0; - THROW_IF_FAILED(font->CreateFontFace(&fontFace0)); - - THROW_IF_FAILED(fontFace0.As(&fontFace)); - - // Retrieve metrics in case the font we created was different than what was requested. - weight = font->GetWeight(); - stretch = font->GetStretch(); - style = font->GetStyle(); - - // Dig the family name out at the end to return it. - familyName = _GetFontFamilyName(fontFamily.Get(), localeName); - } - - return fontFace; -} - -// Routine Description: -// - Helper to retrieve the user's locale preference or fallback to the default. -// Arguments: -// - -// Return Value: -// - A locale that can be used on construction of assorted DX objects that want to know one. -[[nodiscard]] std::wstring DxEngine::_GetLocaleName() const -{ - std::array localeName; - - const auto returnCode = GetUserDefaultLocaleName(localeName.data(), gsl::narrow(localeName.size())); - if (returnCode) - { - return { localeName.data() }; - } - else - { - return { FALLBACK_LOCALE.data(), FALLBACK_LOCALE.size() }; - } -} - -// Routine Description: -// - Retrieves the font family name out of the given object in the given locale. -// - If we can't find a valid name for the given locale, we'll fallback and report it back. -// Arguments: -// - fontFamily - DirectWrite font family object -// - localeName - The locale in which the name should be retrieved. -// - If fallback occurred, this is updated to what we retrieved instead. -// Return Value: -// - Localized string name of the font family -[[nodiscard]] std::wstring DxEngine::_GetFontFamilyName(gsl::not_null const fontFamily, - std::wstring& localeName) const -{ - // See: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontcollection - Microsoft::WRL::ComPtr familyNames; - THROW_IF_FAILED(fontFamily->GetFamilyNames(&familyNames)); - - // First we have to find the right family name for the locale. We're going to bias toward what the caller - // requested, but fallback if we need to and reply with the locale we ended up choosing. - UINT32 index = 0; - BOOL exists = false; - - // This returns S_OK whether or not it finds a locale name. Check exists field instead. - // If it returns an error, it's a real problem, not an absence of this locale name. - // https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-findlocalename - THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists)); - - // If we tried and it still doesn't exist, try with the fallback locale. - if (!exists) - { - localeName = FALLBACK_LOCALE; - THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists)); - } - - // If it still doesn't exist, we're going to try index 0. - if (!exists) - { - index = 0; - - // Get the locale name out so at least the caller knows what locale this name goes with. - UINT32 length = 0; - THROW_IF_FAILED(familyNames->GetLocaleNameLength(index, &length)); - localeName.resize(length); - - // https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getlocalenamelength - // https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getlocalename - // GetLocaleNameLength does not include space for null terminator, but GetLocaleName needs it so add one. - THROW_IF_FAILED(familyNames->GetLocaleName(index, localeName.data(), length + 1)); - } - - // OK, now that we've decided which family name and the locale that it's in... let's go get it. - UINT32 length = 0; - THROW_IF_FAILED(familyNames->GetStringLength(index, &length)); - - // Make our output buffer and resize it so it is allocated. - std::wstring retVal; - retVal.resize(length); - - // FINALLY, go fetch the string name. - // https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getstringlength - // https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getstring - // Once again, GetStringLength is without the null, but GetString needs the null. So add one. - THROW_IF_FAILED(familyNames->GetString(index, retVal.data(), length + 1)); - - // and return it. - return retVal; -} - -// Routine Description: -// - Updates the font used for drawing -// Arguments: -// - desired - Information specifying the font that is requested -// - actual - Filled with the nearest font actually chosen for drawing -// - dpi - The DPI of the screen -// Return Value: -// - S_OK or relevant DirectX error -[[nodiscard]] HRESULT DxEngine::_GetProposedFont(const FontInfoDesired& desired, - FontInfo& actual, - const int dpi, - Microsoft::WRL::ComPtr& textFormat, - Microsoft::WRL::ComPtr& textFormatItalic, - Microsoft::WRL::ComPtr& textAnalyzer, - Microsoft::WRL::ComPtr& fontFace, - Microsoft::WRL::ComPtr& fontFaceItalic, - LineMetrics& lineMetrics) const noexcept -{ - try - { - std::wstring fontName(desired.GetFaceName()); - DWRITE_FONT_WEIGHT weight = static_cast(desired.GetWeight()); - DWRITE_FONT_STYLE style = DWRITE_FONT_STYLE_NORMAL; - DWRITE_FONT_STRETCH stretch = DWRITE_FONT_STRETCH_NORMAL; - std::wstring localeName = _GetLocaleName(); - - // _ResolveFontFaceWithFallback overrides the last argument with the locale name of the font, - // but we should use the system's locale to render the text. - std::wstring fontLocaleName = localeName; - const auto face = _ResolveFontFaceWithFallback(fontName, weight, stretch, style, fontLocaleName); - - DWRITE_FONT_METRICS1 fontMetrics; - face->GetMetrics(&fontMetrics); - - const UINT32 spaceCodePoint = L'M'; - UINT16 spaceGlyphIndex; - THROW_IF_FAILED(face->GetGlyphIndicesW(&spaceCodePoint, 1, &spaceGlyphIndex)); - - INT32 advanceInDesignUnits; - THROW_IF_FAILED(face->GetDesignGlyphAdvances(1, &spaceGlyphIndex, &advanceInDesignUnits)); - - DWRITE_GLYPH_METRICS spaceMetrics = { 0 }; - THROW_IF_FAILED(face->GetDesignGlyphMetrics(&spaceGlyphIndex, 1, &spaceMetrics)); - - // The math here is actually: - // Requested Size in Points * DPI scaling factor * Points to Pixels scaling factor. - // - DPI = dots per inch - // - PPI = points per inch or "points" as usually seen when choosing a font size - // - The DPI scaling factor is the current monitor DPI divided by 96, the default DPI. - // - The Points to Pixels factor is based on the typography definition of 72 points per inch. - // As such, converting requires taking the 96 pixel per inch default and dividing by the 72 points per inch - // to get a factor of 1 and 1/3. - // This turns into something like: - // - 12 ppi font * (96 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 16 pixels tall font for 100% display (96 dpi is 100%) - // - 12 ppi font * (144 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 24 pixels tall font for 150% display (144 dpi is 150%) - // - 12 ppi font * (192 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 32 pixels tall font for 200% display (192 dpi is 200%) - float heightDesired = static_cast(desired.GetEngineSize().Y) * static_cast(USER_DEFAULT_SCREEN_DPI) / POINTS_PER_INCH; - - // The advance is the number of pixels left-to-right (X dimension) for the given font. - // We're finding a proportional factor here with the design units in "ems", not an actual pixel measurement. - - // Now we play trickery with the font size. Scale by the DPI to get the height we expect. - heightDesired *= (static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI)); - - const float widthAdvance = static_cast(advanceInDesignUnits) / fontMetrics.designUnitsPerEm; - - // Use the real pixel height desired by the "em" factor for the width to get the number of pixels - // we will need per character in width. This will almost certainly result in fractional X-dimension pixels. - const float widthApprox = heightDesired * widthAdvance; - - // Since we can't deal with columns of the presentation grid being fractional pixels in width, round to the nearest whole pixel. - const float widthExact = round(widthApprox); - - // Now reverse the "em" factor from above to turn the exact pixel width into a (probably) fractional - // height in pixels of each character. It's easier for us to pad out height and align vertically - // than it is horizontally. - const auto fontSize = widthExact / widthAdvance; - - // Now figure out the basic properties of the character height which include ascent and descent - // for this specific font size. - const float ascent = (fontSize * fontMetrics.ascent) / fontMetrics.designUnitsPerEm; - const float descent = (fontSize * fontMetrics.descent) / fontMetrics.designUnitsPerEm; - - // Get the gap. - const float gap = (fontSize * fontMetrics.lineGap) / fontMetrics.designUnitsPerEm; - const float halfGap = gap / 2; - - // We're going to build a line spacing object here to track all of this data in our format. - DWRITE_LINE_SPACING lineSpacing = {}; - lineSpacing.method = DWRITE_LINE_SPACING_METHOD_UNIFORM; - - // We need to make sure the baseline falls on a round pixel (not a fractional pixel). - // If the baseline is fractional, the text appears blurry, especially at small scales. - // Since we also need to make sure the bounding box as a whole is round pixels - // (because the entire console system maths in full cell units), - // we're just going to ceiling up the ascent and descent to make a full pixel amount - // and set the baseline to the full round pixel ascent value. - // - // For reference, for the letters "ag": - // ... - // gggggg bottom of previous line - // - // ----------------- <===========================================| - // | topSideBearing | 1/2 lineGap | - // aaaaaa ggggggg <-------------------------|-------------| | - // a g g | | | - // aaaaa ggggg |<-ascent | | - // a a g | | |---- lineHeight - // aaaaa a gggggg <----baseline, verticalOriginY----------|---| - // g g |<-descent | | - // gggggg <-------------------------|-------------| | - // | bottomSideBearing | 1/2 lineGap | - // ----------------- <===========================================| - // - // aaaaaa ggggggg top of next line - // ... - // - // Also note... - // We're going to add half the line gap to the ascent and half the line gap to the descent - // to ensure that the spacing is balanced vertically. - // Generally speaking, the line gap is added to the ascent by DirectWrite itself for - // horizontally drawn text which can place the baseline and glyphs "lower" in the drawing - // box than would be desired for proper alignment of things like line and box characters - // which will try to sit centered in the area and touch perfectly with their neighbors. - - const auto fullPixelAscent = ceil(ascent + halfGap); - const auto fullPixelDescent = ceil(descent + halfGap); - lineSpacing.height = fullPixelAscent + fullPixelDescent; - lineSpacing.baseline = fullPixelAscent; - - // According to MSDN (https://docs.microsoft.com/en-us/windows/win32/api/dwrite_3/ne-dwrite_3-dwrite_font_line_gap_usage) - // Setting "ENABLED" means we've included the line gapping in the spacing numbers given. - lineSpacing.fontLineGapUsage = DWRITE_FONT_LINE_GAP_USAGE_ENABLED; - - // Create the font with the fractional pixel height size. - // It should have an integer pixel width by our math above. - // Then below, apply the line spacing to the format to position the floating point pixel height characters - // into a cell that has an integer pixel height leaving some padding above/below as necessary to round them out. - Microsoft::WRL::ComPtr format; - THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontName.data(), - nullptr, - weight, - style, - stretch, - fontSize, - localeName.data(), - &format)); - - THROW_IF_FAILED(format.As(&textFormat)); - - // We also need to create an italic variant of the font face and text - // format, based on the same parameters, but using an italic style. - std::wstring fontNameItalic = fontName; - DWRITE_FONT_WEIGHT weightItalic = weight; - DWRITE_FONT_STYLE styleItalic = DWRITE_FONT_STYLE_ITALIC; - DWRITE_FONT_STRETCH stretchItalic = stretch; - - const auto faceItalic = _ResolveFontFaceWithFallback(fontNameItalic, weightItalic, stretchItalic, styleItalic, fontLocaleName); - - Microsoft::WRL::ComPtr formatItalic; - THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontNameItalic.data(), - nullptr, - weightItalic, - styleItalic, - stretchItalic, - fontSize, - localeName.data(), - &formatItalic)); - - THROW_IF_FAILED(formatItalic.As(&textFormatItalic)); - - Microsoft::WRL::ComPtr analyzer; - THROW_IF_FAILED(_dwriteFactory->CreateTextAnalyzer(&analyzer)); - THROW_IF_FAILED(analyzer.As(&textAnalyzer)); - - fontFace = face; - fontFaceItalic = faceItalic; - - THROW_IF_FAILED(textFormat->SetLineSpacing(lineSpacing.method, lineSpacing.height, lineSpacing.baseline)); - THROW_IF_FAILED(textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR)); - THROW_IF_FAILED(textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP)); - - // The scaled size needs to represent the pixel box that each character will fit within for the purposes - // of hit testing math and other such multiplication/division. - COORD coordSize = { 0 }; - coordSize.X = gsl::narrow(widthExact); - coordSize.Y = gsl::narrow_cast(lineSpacing.height); - - // Unscaled is for the purposes of re-communicating this font back to the renderer again later. - // As such, we need to give the same original size parameter back here without padding - // or rounding or scaling manipulation. - const COORD unscaled = desired.GetEngineSize(); - - const COORD scaled = coordSize; - - actual.SetFromEngine(fontName, - desired.GetFamily(), - textFormat->GetFontWeight(), - false, - scaled, - unscaled); - - // 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. - lineMetrics.gridlineWidth = std::round(fontSize * 0.025f); - - // All other line metrics are in design units, so to get a pixel value, - // we scale by the font size divided by the design-units-per-em. - const auto scale = fontSize / fontMetrics.designUnitsPerEm; - lineMetrics.underlineOffset = std::round(fontMetrics.underlinePosition * scale); - lineMetrics.underlineWidth = std::round(fontMetrics.underlineThickness * scale); - lineMetrics.strikethroughOffset = std::round(fontMetrics.strikethroughPosition * scale); - lineMetrics.strikethroughWidth = std::round(fontMetrics.strikethroughThickness * scale); - - // We always want the lines to be visible, so if a stroke width ends up - // at zero after rounding, we need to make it at least 1 pixel. - lineMetrics.gridlineWidth = std::max(lineMetrics.gridlineWidth, 1.0f); - lineMetrics.underlineWidth = std::max(lineMetrics.underlineWidth, 1.0f); - lineMetrics.strikethroughWidth = std::max(lineMetrics.strikethroughWidth, 1.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. - lineMetrics.underlineOffset = fullPixelAscent - lineMetrics.underlineOffset; - lineMetrics.strikethroughOffset = fullPixelAscent - lineMetrics.strikethroughOffset; - - // 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::round(fontSize * 0.05f); - - // 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 = lineSpacing.height - _lineMetrics.underlineWidth; - lineMetrics.underlineOffset2 = std::min(lineMetrics.underlineOffset2, maxUnderlineOffset); - - // 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; - } - - // We also add half the stroke width to the offsets, since the line - // coordinates designate the center of the line. - lineMetrics.underlineOffset += lineMetrics.underlineWidth / 2.0f; - lineMetrics.underlineOffset2 += lineMetrics.underlineWidth / 2.0f; - lineMetrics.strikethroughOffset += lineMetrics.strikethroughWidth / 2.0f; - } - CATCH_RETURN(); - - return S_OK; -} - // Routine Description: // - Helps convert a GDI COLORREF into a Direct2D ColorF // Arguments: diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index 97f92f8cfc2..39cbfbf8969 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -26,6 +26,7 @@ #include "CustomTextLayout.h" #include "CustomTextRenderer.h" +#include "DxFontRenderData.h" #include "../../types/inc/Viewport.hpp" @@ -155,20 +156,7 @@ namespace Microsoft::Console::Render bool _isEnabled; bool _isPainting; - struct LineMetrics - { - float gridlineWidth; - float underlineOffset; - float underlineOffset2; - float underlineWidth; - float strikethroughOffset; - float strikethroughWidth; - }; - - LineMetrics _lineMetrics; til::size _displaySizePixels; - til::size _glyphCell; - ::Microsoft::WRL::ComPtr _boxDrawingEffect; D2D1_COLOR_F _defaultForegroundColor; D2D1_COLOR_F _defaultBackgroundColor; @@ -198,17 +186,14 @@ namespace Microsoft::Console::Render ::Microsoft::WRL::ComPtr _d2dFactory; ::Microsoft::WRL::ComPtr _dwriteFactory; - ::Microsoft::WRL::ComPtr _dwriteTextFormat; - ::Microsoft::WRL::ComPtr _dwriteTextFormatItalic; - ::Microsoft::WRL::ComPtr _dwriteFontFace; - ::Microsoft::WRL::ComPtr _dwriteFontFaceItalic; - ::Microsoft::WRL::ComPtr _dwriteTextAnalyzer; ::Microsoft::WRL::ComPtr _customLayout; ::Microsoft::WRL::ComPtr _customRenderer; ::Microsoft::WRL::ComPtr _strokeStyle; ::Microsoft::WRL::ComPtr _dashStrokeStyle; ::Microsoft::WRL::ComPtr _hyperlinkStrokeStyle; + std::unique_ptr _fontRenderData; + D2D1_STROKE_STYLE_PROPERTIES _strokeStyleProperties; D2D1_STROKE_STYLE_PROPERTIES _dashStrokeStyleProperties; @@ -303,33 +288,6 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT _EnableDisplayAccess(const bool outputEnabled) noexcept; - [[nodiscard]] ::Microsoft::WRL::ComPtr _ResolveFontFaceWithFallback(std::wstring& familyName, - DWRITE_FONT_WEIGHT& weight, - DWRITE_FONT_STRETCH& stretch, - DWRITE_FONT_STYLE& style, - std::wstring& localeName) const; - - [[nodiscard]] ::Microsoft::WRL::ComPtr _FindFontFace(std::wstring& familyName, - DWRITE_FONT_WEIGHT& weight, - DWRITE_FONT_STRETCH& stretch, - DWRITE_FONT_STYLE& style, - std::wstring& localeName) const; - - [[nodiscard]] std::wstring _GetLocaleName() const; - - [[nodiscard]] std::wstring _GetFontFamilyName(gsl::not_null const fontFamily, - std::wstring& localeName) const; - - [[nodiscard]] HRESULT _GetProposedFont(const FontInfoDesired& desired, - FontInfo& actual, - const int dpi, - ::Microsoft::WRL::ComPtr& textFormat, - ::Microsoft::WRL::ComPtr& textFormatItalic, - ::Microsoft::WRL::ComPtr& textAnalyzer, - ::Microsoft::WRL::ComPtr& fontFace, - ::Microsoft::WRL::ComPtr& fontFaceItalic, - LineMetrics& lineMetrics) const noexcept; - [[nodiscard]] til::size _GetClientSize() const; void _InvalidateRectangle(const til::rectangle& rc); diff --git a/src/renderer/dx/lib/dx.vcxproj b/src/renderer/dx/lib/dx.vcxproj index 8eb6ba6e9aa..61f2ca24992 100644 --- a/src/renderer/dx/lib/dx.vcxproj +++ b/src/renderer/dx/lib/dx.vcxproj @@ -21,6 +21,7 @@ Create + @@ -29,6 +30,7 @@ + diff --git a/src/renderer/dx/lib/dx.vcxproj.filters b/src/renderer/dx/lib/dx.vcxproj.filters index 4a0cc0df7dc..86916f13958 100644 --- a/src/renderer/dx/lib/dx.vcxproj.filters +++ b/src/renderer/dx/lib/dx.vcxproj.filters @@ -7,6 +7,7 @@ + @@ -14,6 +15,7 @@ + diff --git a/src/renderer/dx/sources.inc b/src/renderer/dx/sources.inc index 34592d022b2..328779d27c4 100644 --- a/src/renderer/dx/sources.inc +++ b/src/renderer/dx/sources.inc @@ -33,6 +33,7 @@ INCLUDES = \ SOURCES = \ $(SOURCES) \ ..\DxRenderer.cpp \ + ..\DxFontRenderData.cpp \ ..\CustomTextRenderer.cpp \ ..\CustomTextLayout.cpp \