From eaf86b08b1d28a8b3277e182abf074ed38ea4dcb Mon Sep 17 00:00:00 2001 From: Chester Liu Date: Thu, 18 Feb 2021 17:05:50 +0800 Subject: [PATCH 01/11] Introduce DxFontInfo --- src/renderer/dx/CustomTextLayout.cpp | 2 + src/renderer/dx/DxFontInfo.cpp | 251 ++++++++++ src/renderer/dx/DxFontInfo.h | 59 +++ src/renderer/dx/DxFontRenderData.cpp | 669 +++++++++++---------------- src/renderer/dx/DxFontRenderData.h | 39 +- src/renderer/dx/lib/dx.vcxproj | 2 + src/renderer/dx/sources.inc | 1 + 7 files changed, 599 insertions(+), 424 deletions(-) create mode 100644 src/renderer/dx/DxFontInfo.cpp create mode 100644 src/renderer/dx/DxFontInfo.h diff --git a/src/renderer/dx/CustomTextLayout.cpp b/src/renderer/dx/CustomTextLayout.cpp index f051026d551..f3446d602b8 100644 --- a/src/renderer/dx/CustomTextLayout.cpp +++ b/src/renderer/dx/CustomTextLayout.cpp @@ -133,6 +133,7 @@ CATCH_RETURN() _In_ IDWriteTextRenderer* renderer, FLOAT originX, FLOAT originY) noexcept +try { const auto drawingContext = static_cast(clientDrawingContext); _formatInUse = drawingContext->useItalicFont ? _fontRenderData->ItalicTextFormat().Get() : _fontRenderData->DefaultTextFormat().Get(); @@ -151,6 +152,7 @@ CATCH_RETURN() return S_OK; } +CATCH_RETURN() // Routine Description: // - Uses the internal text information and the analyzers/font information from construction diff --git a/src/renderer/dx/DxFontInfo.cpp b/src/renderer/dx/DxFontInfo.cpp new file mode 100644 index 00000000000..0ab118be5c4 --- /dev/null +++ b/src/renderer/dx/DxFontInfo.cpp @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "DxFontInfo.h" + +static constexpr std::wstring_view FALLBACK_FONT_FACES[] = { L"Consolas", L"Lucida Console", L"Courier New" }; + +using namespace Microsoft::Console::Render; + + +DxFontInfo::DxFontInfo() noexcept: + _familyName(), + _weight(DWRITE_FONT_WEIGHT_NORMAL), + _style(DWRITE_FONT_STYLE_NORMAL), + _stretch(DWRITE_FONT_STRETCH_NORMAL) +{ +} + +DxFontInfo::DxFontInfo(std::wstring_view familyName, + DWRITE_FONT_WEIGHT weight, + DWRITE_FONT_STYLE style, + DWRITE_FONT_STRETCH stretch) : + _familyName(familyName), + _weight(weight), + _style(style), + _stretch(stretch) +{ +} + +DxFontInfo::DxFontInfo(std::wstring_view familyName, + unsigned int weight, + DWRITE_FONT_STYLE style, + DWRITE_FONT_STRETCH stretch) : + _familyName(familyName), + _weight(static_cast(weight)), + _style(style), + _stretch(stretch) +{ +} + +std::wstring DxFontInfo::GetFamilyName() const +{ + return _familyName; +} + +void DxFontInfo::SetFamilyName(const std::wstring_view familyName) +{ + _familyName = familyName; +} + +DWRITE_FONT_WEIGHT DxFontInfo::GetWeight() const noexcept +{ + return _weight; +} + +void DxFontInfo::SetWeight(const DWRITE_FONT_WEIGHT weight) noexcept +{ + _weight = weight; +} + +DWRITE_FONT_STYLE DxFontInfo::GetStyle() const noexcept +{ + return _style; +} + +void DxFontInfo::SetStyle(const DWRITE_FONT_STYLE style) noexcept +{ + _style = style; +} + +DWRITE_FONT_STRETCH DxFontInfo::GetStretch() const noexcept +{ + return _stretch; +} + +void DxFontInfo::SetStretch(const DWRITE_FONT_STRETCH stretch) noexcept +{ + _stretch = stretch; +} + +void DxFontInfo::SetFromEngine(const std::wstring_view familyName, + const DWRITE_FONT_WEIGHT weight, + const DWRITE_FONT_STYLE style, + const DWRITE_FONT_STRETCH stretch) +{ + _familyName = familyName; + _weight = weight; + _style = style; + _stretch = stretch; +} + +// 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 DxFontInfo::ResolveFontFaceWithFallback(gsl::not_null dwriteFactory, + std::wstring& localeName) +{ + auto face = _FindFontFace(dwriteFactory, localeName); + + if (!face) + { + for (const auto fallbackFace : FALLBACK_FONT_FACES) + { + _familyName = fallbackFace; + face = _FindFontFace(dwriteFactory, localeName); + + if (face) + { + break; + } + + SetFromEngine(fallbackFace, + DWRITE_FONT_WEIGHT_NORMAL, + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL); + face = _FindFontFace(dwriteFactory, 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 DxFontInfo::_FindFontFace(gsl::not_null dwriteFactory, std::wstring& localeName) +{ + Microsoft::WRL::ComPtr fontFace; + + Microsoft::WRL::ComPtr fontCollection; + THROW_IF_FAILED(dwriteFactory->GetSystemFontCollection(&fontCollection, false)); + + UINT32 familyIndex; + BOOL familyExists; + THROW_IF_FAILED(fontCollection->FindFamilyName(GetFamilyName().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(GetWeight(), GetStretch(), GetStyle(), &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. + SetWeight(font->GetWeight()); + SetStretch(font->GetStretch()); + SetStyle(font->GetStyle()); + + // Dig the family name out at the end to return it. + SetFamilyName(_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 DxFontInfo::_GetFontFamilyName(gsl::not_null const fontFamily, + std::wstring& localeName) +{ + // 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 = L"en-us"; + 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; +} diff --git a/src/renderer/dx/DxFontInfo.h b/src/renderer/dx/DxFontInfo.h new file mode 100644 index 00000000000..0463bd60f6a --- /dev/null +++ b/src/renderer/dx/DxFontInfo.h @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include +#include +#include +#include + +namespace Microsoft::Console::Render +{ + class DxFontInfo + { + public: + DxFontInfo() noexcept; + + DxFontInfo(std::wstring_view familyName, + DWRITE_FONT_WEIGHT weight, + DWRITE_FONT_STYLE style, + DWRITE_FONT_STRETCH stretch); + + DxFontInfo(std::wstring_view familyName, + unsigned int weight, + DWRITE_FONT_STYLE style, + DWRITE_FONT_STRETCH stretch); + + std::wstring GetFamilyName() const; + void SetFamilyName(const std::wstring_view familyName); + + DWRITE_FONT_WEIGHT GetWeight() const noexcept; + void SetWeight(const DWRITE_FONT_WEIGHT weight) noexcept; + + DWRITE_FONT_STYLE GetStyle() const noexcept; + void SetStyle(const DWRITE_FONT_STYLE style) noexcept; + + DWRITE_FONT_STRETCH GetStretch() const noexcept; + void SetStretch(const DWRITE_FONT_STRETCH stretch) noexcept; + + void SetFromEngine(const std::wstring_view familyName, + const DWRITE_FONT_WEIGHT weight, + const DWRITE_FONT_STYLE style, + const DWRITE_FONT_STRETCH stretch); + + [[nodiscard]] ::Microsoft::WRL::ComPtr ResolveFontFaceWithFallback(gsl::not_null dwriteFactory, + std::wstring& localeName); + + private: + [[nodiscard]] ::Microsoft::WRL::ComPtr _FindFontFace(gsl::not_null dwriteFactory, std::wstring& localeName); + + [[nodiscard]] std::wstring _GetFontFamilyName(gsl::not_null const fontFamily, + std::wstring& localeName); + + std::wstring _familyName; + DWRITE_FONT_WEIGHT _weight; + DWRITE_FONT_STYLE _style; + DWRITE_FONT_STRETCH _stretch; + }; +} diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp index 0c431f2bf4c..c96d410fbb7 100644 --- a/src/renderer/dx/DxFontRenderData.cpp +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -13,14 +13,22 @@ using namespace Microsoft::Console::Render; DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwriteFactory) noexcept : _dwriteFactory(dwriteFactory), + _fontSize{}, _glyphCell{}, - _lineMetrics({}), - _boxDrawingEffect{} + _lineMetrics{}, + _lineSpacing{} { } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::Analyzer() noexcept +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::Analyzer() { + if (!_dwriteTextAnalyzer) + { + Microsoft::WRL::ComPtr analyzer; + THROW_IF_FAILED(_dwriteFactory->CreateTextAnalyzer(&analyzer)); + THROW_IF_FAILED(analyzer.As(&_dwriteTextAnalyzer)); + } + return _dwriteTextAnalyzer; } @@ -36,6 +44,26 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr return _systemFontFallback; } +[[nodiscard]] std::wstring DxFontRenderData::UserLocaleName() +{ + 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; +} + [[nodiscard]] til::size DxFontRenderData::GlyphCell() noexcept { return _glyphCell; @@ -46,28 +74,73 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr return _lineMetrics; } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultTextFormat() noexcept +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultTextFormat() { + if (!_dwriteTextFormat) + { + // Create the font with the fractional pixel height size. + // It should have an integer pixel width by our math. + // 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. + std::wstring localeName = UserLocaleName(); + THROW_IF_FAILED(_BuildTextFormat(_defaultFontInfo, localeName).As(&_dwriteTextFormat)); + 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)); + } return _dwriteTextFormat; } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultFontFace() noexcept +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultFontFace() { + if (!_dwriteFontFace) + { + std::wstring fontLocaleName = UserLocaleName(); + // _ResolveFontFaceWithFallback overrides the last argument with the locale name of the font, + // but we should use the system's locale to render the text. + _dwriteFontFace = _defaultFontInfo.ResolveFontFaceWithFallback(_dwriteFactory.Get(), fontLocaleName); + } + return _dwriteFontFace; } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultBoxDrawingEffect() noexcept +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultBoxDrawingEffect() { + if (!_boxDrawingEffect) + { + // 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. + THROW_IF_FAILED(s_CalculateBoxEffect(DefaultTextFormat().Get(), _glyphCell.width(), DefaultFontFace().Get(), 1.0f, &_boxDrawingEffect)); + } + return _boxDrawingEffect; } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::ItalicTextFormat() noexcept +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::ItalicTextFormat() { + if (!_dwriteTextFormatItalic) + { + std::wstring localeName = UserLocaleName(); + DxFontInfo fontInfoItalic = _defaultFontInfo; + fontInfoItalic.SetStyle(DWRITE_FONT_STYLE_ITALIC); + THROW_IF_FAILED(_BuildTextFormat(fontInfoItalic, localeName).As(&_dwriteTextFormatItalic)); + THROW_IF_FAILED(_dwriteTextFormatItalic->SetLineSpacing(_lineSpacing.method, _lineSpacing.height, _lineSpacing.baseline)); + THROW_IF_FAILED(_dwriteTextFormatItalic->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR)); + THROW_IF_FAILED(_dwriteTextFormatItalic->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP)); + } + return _dwriteTextFormatItalic; } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::ItalicFontFace() noexcept +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::ItalicFontFace() { + if (!_dwriteFontFaceItalic) + { + std::wstring fontLocaleName = UserLocaleName(); + DxFontInfo fontInfoItalic = _defaultFontInfo; + fontInfoItalic.SetStyle(DWRITE_FONT_STYLE_ITALIC); + _dwriteFontFaceItalic = fontInfoItalic.ResolveFontFaceWithFallback(_dwriteFactory.Get(), fontLocaleName); + } + return _dwriteFontFaceItalic; } @@ -84,243 +157,19 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr 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)); + _dwriteTextFormat.Reset(); + _dwriteFontFace.Reset(); + _boxDrawingEffect.Reset(); + _dwriteTextFormatItalic.Reset(); + _dwriteFontFaceItalic.Reset(); + + // Initialize the default font info and build everything from here. + _defaultFontInfo = DxFontInfo(desired.GetFaceName(), + desired.GetWeight(), + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL); + + _BuildFontRenderData(desired, actual, dpi); } CATCH_RETURN(); @@ -569,187 +418,207 @@ try 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. +// - Build the needed data for rendering according to the font used // 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. +// - 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: -// - 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); +// - None +void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi) +{ + const auto face = DefaultFontFace(); - 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; - } - } - } + DWRITE_FONT_METRICS1 fontMetrics; + face->GetMetrics(&fontMetrics); - THROW_HR_IF_NULL(E_FAIL, face); + const UINT32 spaceCodePoint = L'M'; + UINT16 spaceGlyphIndex; + THROW_IF_FAILED(face->GetGlyphIndicesW(&spaceCodePoint, 1, &spaceGlyphIndex)); - return face; -} + INT32 advanceInDesignUnits; + THROW_IF_FAILED(face->GetDesignGlyphAdvances(1, &spaceGlyphIndex, &advanceInDesignUnits)); -// 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; + DWRITE_GLYPH_METRICS spaceMetrics = { 0 }; + THROW_IF_FAILED(face->GetDesignGlyphMetrics(&spaceGlyphIndex, 1, &spaceMetrics)); - Microsoft::WRL::ComPtr fontCollection; - THROW_IF_FAILED(_dwriteFactory->GetSystemFontCollection(&fontCollection, false)); + // 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; - UINT32 familyIndex; - BOOL familyExists; - THROW_IF_FAILED(fontCollection->FindFamilyName(familyName.data(), &familyIndex, &familyExists)); + // 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. - if (familyExists) - { - Microsoft::WRL::ComPtr fontFamily; - THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily)); + // 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)); - Microsoft::WRL::ComPtr font; - THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(weight, stretch, style, &font)); + const float widthAdvance = static_cast(advanceInDesignUnits) / fontMetrics.designUnitsPerEm; - Microsoft::WRL::ComPtr fontFace0; - THROW_IF_FAILED(font->CreateFontFace(&fontFace0)); + // 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; - THROW_IF_FAILED(fontFace0.As(&fontFace)); + // 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); - // Retrieve metrics in case the font we created was different than what was requested. - weight = font->GetWeight(); - stretch = font->GetStretch(); - style = font->GetStyle(); + // 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; + _fontSize = fontSize; - // Dig the family name out at the end to return it. - familyName = _GetFontFamilyName(fontFamily.Get(), localeName); - } + // 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; - return fontFace; -} + // Get the gap. + const float gap = (fontSize * fontMetrics.lineGap) / fontMetrics.designUnitsPerEm; + const float halfGap = gap / 2; -// 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)); - } + // 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; - // If it still doesn't exist, we're going to try index 0. - if (!exists) - { - index = 0; + // 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. - // 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); + const auto fullPixelAscent = ceil(ascent + halfGap); + const auto fullPixelDescent = ceil(descent + halfGap); + lineSpacing.height = fullPixelAscent + fullPixelDescent; + lineSpacing.baseline = fullPixelAscent; - // 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)); - } + // 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; - // 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)); + _lineSpacing = lineSpacing; - // Make our output buffer and resize it so it is allocated. - std::wstring retVal; - retVal.resize(length); + // 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); - // 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)); + // 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(); - // and return it. - return retVal; -} + const COORD scaled = coordSize; -[[nodiscard]] std::wstring DxFontRenderData::_GetUserLocaleName() -{ - if (_userLocaleName.empty()) - { - std::array localeName; + actual.SetFromEngine(_defaultFontInfo.GetFamilyName(), + desired.GetFamily(), + DefaultTextFormat()->GetFontWeight(), + false, + scaled, + unscaled); - const auto returnCode = GetUserDefaultLocaleName(localeName.data(), gsl::narrow(localeName.size())); - if (returnCode) - { - _userLocaleName = { localeName.data() }; - } - else + 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) { - _userLocaleName = { FALLBACK_LOCALE.data(), FALLBACK_LOCALE.size() }; + lineMetrics.underlineOffset2 = lineMetrics.underlineOffset - lineMetrics.gridlineWidth; } - } - return _userLocaleName; + // 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(); +} + + +Microsoft::WRL::ComPtr DxFontRenderData::_BuildTextFormat(const DxFontInfo fontInfo, const std::wstring_view localeName) +{ + Microsoft::WRL::ComPtr format; + THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontInfo.GetFamilyName().data(), + nullptr, + fontInfo.GetWeight(), + fontInfo.GetStyle(), + fontInfo.GetStretch(), + _fontSize, + localeName.data(), + &format)); + return format; } diff --git a/src/renderer/dx/DxFontRenderData.h b/src/renderer/dx/DxFontRenderData.h index 403d323e565..d8a2cd507b7 100644 --- a/src/renderer/dx/DxFontRenderData.h +++ b/src/renderer/dx/DxFontRenderData.h @@ -4,6 +4,7 @@ #pragma once #include "../../renderer/inc/FontInfoDesired.hpp" +#include "DxFontInfo.h" #include "BoxDrawingEffect.h" #include @@ -31,50 +32,38 @@ namespace Microsoft::Console::Render DxFontRenderData(::Microsoft::WRL::ComPtr dwriteFactory) noexcept; // DirectWrite text analyzer from the factory - [[nodiscard]] Microsoft::WRL::ComPtr Analyzer() noexcept; + [[nodiscard]] Microsoft::WRL::ComPtr Analyzer(); [[nodiscard]] Microsoft::WRL::ComPtr SystemFontFallback(); + // A locale that can be used on construction of assorted DX objects that want to know one. + [[nodiscard]] std::wstring UserLocaleName(); + [[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; + [[nodiscard]] Microsoft::WRL::ComPtr DefaultTextFormat(); // The DirectWrite font face to use while calculating layout (by default) - [[nodiscard]] Microsoft::WRL::ComPtr DefaultFontFace() noexcept; + [[nodiscard]] Microsoft::WRL::ComPtr DefaultFontFace(); // Box drawing scaling effects that are cached for the base font across layouts - [[nodiscard]] Microsoft::WRL::ComPtr DefaultBoxDrawingEffect() noexcept; + [[nodiscard]] Microsoft::WRL::ComPtr DefaultBoxDrawingEffect(); // The italic variant of the format object representing the size and other text properties for italic text - [[nodiscard]] Microsoft::WRL::ComPtr ItalicTextFormat() noexcept; + [[nodiscard]] Microsoft::WRL::ComPtr ItalicTextFormat(); // The italic variant of the font face to use while calculating layout for italic text - [[nodiscard]] Microsoft::WRL::ComPtr ItalicFontFace() noexcept; + [[nodiscard]] Microsoft::WRL::ComPtr ItalicFontFace(); [[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(); + void _BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi); + Microsoft::WRL::ComPtr _BuildTextFormat(const DxFontInfo fontInfo, const std::wstring_view localeName); ::Microsoft::WRL::ComPtr _dwriteFactory; @@ -88,9 +77,11 @@ namespace Microsoft::Console::Render ::Microsoft::WRL::ComPtr _systemFontFallback; std::wstring _userLocaleName; + DxFontInfo _defaultFontInfo; + float _fontSize; til::size _glyphCell; - + DWRITE_LINE_SPACING _lineSpacing; LineMetrics _lineMetrics; }; } diff --git a/src/renderer/dx/lib/dx.vcxproj b/src/renderer/dx/lib/dx.vcxproj index 61f2ca24992..618d1b9ce82 100644 --- a/src/renderer/dx/lib/dx.vcxproj +++ b/src/renderer/dx/lib/dx.vcxproj @@ -21,6 +21,7 @@ Create + @@ -30,6 +31,7 @@ + diff --git a/src/renderer/dx/sources.inc b/src/renderer/dx/sources.inc index 328779d27c4..f94f2d5ba32 100644 --- a/src/renderer/dx/sources.inc +++ b/src/renderer/dx/sources.inc @@ -33,6 +33,7 @@ INCLUDES = \ SOURCES = \ $(SOURCES) \ ..\DxRenderer.cpp \ + ..\DxFontInfo.cpp \ ..\DxFontRenderData.cpp \ ..\CustomTextRenderer.cpp \ ..\CustomTextLayout.cpp \ From b1fc5bebfbd78832f3e3f81c3ba943166f383bd5 Mon Sep 17 00:00:00 2001 From: Chester Liu Date: Thu, 18 Feb 2021 18:30:26 +0800 Subject: [PATCH 02/11] Format --- src/renderer/dx/DxFontInfo.cpp | 3 +- src/renderer/dx/DxFontRenderData.cpp | 369 +++++++++++++-------------- 2 files changed, 185 insertions(+), 187 deletions(-) diff --git a/src/renderer/dx/DxFontInfo.cpp b/src/renderer/dx/DxFontInfo.cpp index 0ab118be5c4..e59ea34986f 100644 --- a/src/renderer/dx/DxFontInfo.cpp +++ b/src/renderer/dx/DxFontInfo.cpp @@ -9,8 +9,7 @@ static constexpr std::wstring_view FALLBACK_FONT_FACES[] = { L"Consolas", L"Luci using namespace Microsoft::Console::Render; - -DxFontInfo::DxFontInfo() noexcept: +DxFontInfo::DxFontInfo() noexcept : _familyName(), _weight(DWRITE_FONT_WEIGHT_NORMAL), _style(DWRITE_FONT_STYLE_NORMAL), diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp index c96d410fbb7..dfdf8556ae8 100644 --- a/src/renderer/dx/DxFontRenderData.cpp +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -426,199 +426,198 @@ CATCH_RETURN() // Return Value: // - None void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi) -{ - const auto face = DefaultFontFace(); - - 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; - _fontSize = fontSize; - - // 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; - - _lineSpacing = lineSpacing; - - // 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(_defaultFontInfo.GetFamilyName(), - desired.GetFamily(), - DefaultTextFormat()->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; - } +{ + const auto face = DefaultFontFace(); + + 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; + _fontSize = fontSize; + + // 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; + + _lineSpacing = lineSpacing; + + // 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(_defaultFontInfo.GetFamilyName(), + desired.GetFamily(), + DefaultTextFormat()->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; + // 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; + _lineMetrics = lineMetrics; - _glyphCell = actual.GetSize(); + _glyphCell = actual.GetSize(); } - Microsoft::WRL::ComPtr DxFontRenderData::_BuildTextFormat(const DxFontInfo fontInfo, const std::wstring_view localeName) { Microsoft::WRL::ComPtr format; THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontInfo.GetFamilyName().data(), - nullptr, - fontInfo.GetWeight(), - fontInfo.GetStyle(), - fontInfo.GetStretch(), - _fontSize, - localeName.data(), - &format)); + nullptr, + fontInfo.GetWeight(), + fontInfo.GetStyle(), + fontInfo.GetStretch(), + _fontSize, + localeName.data(), + &format)); return format; } From 8456d8106abedc3237bed2c4e2635e7b4f6546fc Mon Sep 17 00:00:00 2001 From: Chester Liu Date: Fri, 9 Apr 2021 13:40:56 +0800 Subject: [PATCH 03/11] clean --- src/renderer/dx/DxFontInfo.cpp | 32 +++++++++++++++----------- src/renderer/dx/DxFontInfo.h | 1 + src/renderer/dx/DxFontRenderData.cpp | 34 ---------------------------- src/renderer/dx/DxFontRenderData.h | 4 ---- 4 files changed, 20 insertions(+), 51 deletions(-) diff --git a/src/renderer/dx/DxFontInfo.cpp b/src/renderer/dx/DxFontInfo.cpp index b38491639e2..77521851d95 100644 --- a/src/renderer/dx/DxFontInfo.cpp +++ b/src/renderer/dx/DxFontInfo.cpp @@ -17,7 +17,8 @@ DxFontInfo::DxFontInfo() noexcept : _familyName(), _weight(DWRITE_FONT_WEIGHT_NORMAL), _style(DWRITE_FONT_STYLE_NORMAL), - _stretch(DWRITE_FONT_STRETCH_NORMAL) + _stretch(DWRITE_FONT_STRETCH_NORMAL), + _didFallback(false) { } @@ -28,7 +29,8 @@ DxFontInfo::DxFontInfo(std::wstring_view familyName, _familyName(familyName), _weight(weight), _style(style), - _stretch(stretch) + _stretch(stretch), + _didFallback(false) { } @@ -39,7 +41,8 @@ DxFontInfo::DxFontInfo(std::wstring_view familyName, _familyName(familyName), _weight(static_cast(weight)), _style(style), - _stretch(stretch) + _stretch(stretch), + _didFallback(false) { } @@ -105,10 +108,8 @@ void DxFontInfo::SetFromEngine(const std::wstring_view familyName, // 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. +// - dwriteFactory - The DWrite factory to use +// - localeName - Locale to search for appropriate fonts // Return Value: // - Smart pointer holding interface reference for queryable font data. [[nodiscard]] Microsoft::WRL::ComPtr DxFontInfo::ResolveFontFaceWithFallback(gsl::not_null dwriteFactory, @@ -185,10 +186,8 @@ void DxFontInfo::SetFromEngine(const std::wstring_view familyName, // 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. +// - dwriteFactory - The DWrite factory to use +// - localeName - Locale to search for appropriate fonts // Return Value: // - Smart pointer holding interface reference for queryable font data. [[nodiscard]] Microsoft::WRL::ComPtr DxFontInfo::_FindFontFace(gsl::not_null dwriteFactory, std::wstring& localeName) @@ -202,7 +201,7 @@ void DxFontInfo::SetFromEngine(const std::wstring_view familyName, BOOL familyExists; THROW_IF_FAILED(fontCollection->FindFamilyName(_familyName.data(), &familyIndex, &familyExists)); - // If the system collection missed, try the files sitting next to our binary. + // If the system collection missed, try the files sitting next to our binary. if (!familyExists) { auto&& nearbyCollection = NearbyCollection(dwriteFactory); @@ -311,7 +310,7 @@ void DxFontInfo::SetFromEngine(const std::wstring_view familyName, // - Creates a DirectWrite font collection of font files that are sitting next to the running // binary (in the same directory as the EXE). // Arguments: -// - +// - dwriteFactory - The DWrite factory to use // Return Value: // - DirectWrite font collection. May be null if one cannot be created. [[nodiscard]] const Microsoft::WRL::ComPtr& DxFontInfo::NearbyCollection(gsl::not_null dwriteFactory) const @@ -352,6 +351,13 @@ void DxFontInfo::SetFromEngine(const std::wstring_view familyName, return _nearbyCollection; } +// Routine Description: +// - Digs through the directory that the current executable is running within to find +// any TTF files sitting next to it. +// Arguments: +// - +// Return Value: +// - Iterable collection of filesystem paths, one per font file that was found [[nodiscard]] std::vector DxFontInfo::s_GetNearbyFonts() { std::vector paths; diff --git a/src/renderer/dx/DxFontInfo.h b/src/renderer/dx/DxFontInfo.h index 99a5bb22022..36ba4683588 100644 --- a/src/renderer/dx/DxFontInfo.h +++ b/src/renderer/dx/DxFontInfo.h @@ -59,6 +59,7 @@ namespace Microsoft::Console::Render mutable ::Microsoft::WRL::ComPtr _nearbyCollection; + // The font name we should be looking for std::wstring _familyName; // The weight (bold, light, etc.) diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp index b27f9b124e7..ccc8adb8fb4 100644 --- a/src/renderer/dx/DxFontRenderData.cpp +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -627,37 +627,3 @@ Microsoft::WRL::ComPtr DxFontRenderData::_BuildTextFormat(con &format)); return format; } - -// Routine Description: -// - Digs through the directory that the current executable is running within to find -// any TTF files sitting next to it. -// Arguments: -// - -// Return Value: -// - Iterable collection of filesystem paths, one per font file that was found -[[nodiscard]] std::vector DxFontRenderData::s_GetNearbyFonts() -{ - std::vector paths; - - // Find the directory we're running from then enumerate all the TTF files - // sitting next to us. - const std::filesystem::path module{ wil::GetModuleFileNameW(nullptr) }; - const auto folder{ module.parent_path() }; - - for (auto& p : std::filesystem::directory_iterator(folder)) - { - if (p.is_regular_file()) - { - auto extension = p.path().extension().wstring(); - std::transform(extension.begin(), extension.end(), extension.begin(), std::towlower); - - static constexpr std::wstring_view ttfExtension{ L".ttf" }; - if (ttfExtension == extension) - { - paths.push_back(p); - } - } - } - - return paths; -} diff --git a/src/renderer/dx/DxFontRenderData.h b/src/renderer/dx/DxFontRenderData.h index e9364422565..63f4528ddc4 100644 --- a/src/renderer/dx/DxFontRenderData.h +++ b/src/renderer/dx/DxFontRenderData.h @@ -39,8 +39,6 @@ namespace Microsoft::Console::Render // A locale that can be used on construction of assorted DX objects that want to know one. [[nodiscard]] std::wstring UserLocaleName(); - [[nodiscard]] const Microsoft::WRL::ComPtr& NearbyCollection() const; - [[nodiscard]] til::size GlyphCell() noexcept; [[nodiscard]] LineMetrics GetLineMetrics() noexcept; @@ -67,8 +65,6 @@ namespace Microsoft::Console::Render void _BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi); Microsoft::WRL::ComPtr _BuildTextFormat(const DxFontInfo fontInfo, const std::wstring_view localeName); - [[nodiscard]] static std::vector s_GetNearbyFonts(); - ::Microsoft::WRL::ComPtr _dwriteFactory; ::Microsoft::WRL::ComPtr _dwriteTextAnalyzer; From 4f5e4fca911a3322634174b5fd9c27dc183fa8f0 Mon Sep 17 00:00:00 2001 From: skyline75489 Date: Thu, 15 Apr 2021 14:45:05 +0800 Subject: [PATCH 04/11] Feedback --- src/renderer/dx/CustomTextLayout.cpp | 4 +--- src/renderer/dx/CustomTextLayout.h | 2 +- src/renderer/dx/DxFontInfo.cpp | 14 +++++--------- src/renderer/dx/DxFontInfo.h | 6 +++--- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/renderer/dx/CustomTextLayout.cpp b/src/renderer/dx/CustomTextLayout.cpp index f3446d602b8..dd66c7ee22f 100644 --- a/src/renderer/dx/CustomTextLayout.cpp +++ b/src/renderer/dx/CustomTextLayout.cpp @@ -132,8 +132,7 @@ CATCH_RETURN() [[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::Draw(_In_opt_ void* clientDrawingContext, _In_ IDWriteTextRenderer* renderer, FLOAT originX, - FLOAT originY) noexcept -try + FLOAT originY) { const auto drawingContext = static_cast(clientDrawingContext); _formatInUse = drawingContext->useItalicFont ? _fontRenderData->ItalicTextFormat().Get() : _fontRenderData->DefaultTextFormat().Get(); @@ -152,7 +151,6 @@ try return S_OK; } -CATCH_RETURN() // Routine Description: // - Uses the internal text information and the analyzers/font information from construction diff --git a/src/renderer/dx/CustomTextLayout.h b/src/renderer/dx/CustomTextLayout.h index 2ac04278bf1..081abab03f1 100644 --- a/src/renderer/dx/CustomTextLayout.h +++ b/src/renderer/dx/CustomTextLayout.h @@ -33,7 +33,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT STDMETHODCALLTYPE Draw(_In_opt_ void* clientDrawingContext, _In_ IDWriteTextRenderer* renderer, FLOAT originX, - FLOAT originY) noexcept; + FLOAT originY); // IDWriteTextAnalysisSource methods [[nodiscard]] HRESULT STDMETHODCALLTYPE GetTextAtPosition(UINT32 textPosition, diff --git a/src/renderer/dx/DxFontInfo.cpp b/src/renderer/dx/DxFontInfo.cpp index 77521851d95..08ff8e7b368 100644 --- a/src/renderer/dx/DxFontInfo.cpp +++ b/src/renderer/dx/DxFontInfo.cpp @@ -23,30 +23,26 @@ DxFontInfo::DxFontInfo() noexcept : } DxFontInfo::DxFontInfo(std::wstring_view familyName, - DWRITE_FONT_WEIGHT weight, + unsigned int weight, DWRITE_FONT_STYLE style, DWRITE_FONT_STRETCH stretch) : - _familyName(familyName), - _weight(weight), - _style(style), - _stretch(stretch), - _didFallback(false) + DxFontInfo(familyName, static_cast(weight), style, stretch) { } DxFontInfo::DxFontInfo(std::wstring_view familyName, - unsigned int weight, + DWRITE_FONT_WEIGHT weight, DWRITE_FONT_STYLE style, DWRITE_FONT_STRETCH stretch) : _familyName(familyName), - _weight(static_cast(weight)), + _weight(weight), _style(style), _stretch(stretch), _didFallback(false) { } -std::wstring DxFontInfo::GetFamilyName() const +std::wstring_view DxFontInfo::GetFamilyName() const noexcept { return _familyName; } diff --git a/src/renderer/dx/DxFontInfo.h b/src/renderer/dx/DxFontInfo.h index 36ba4683588..676bc99ea5e 100644 --- a/src/renderer/dx/DxFontInfo.h +++ b/src/renderer/dx/DxFontInfo.h @@ -16,16 +16,16 @@ namespace Microsoft::Console::Render DxFontInfo() noexcept; DxFontInfo(std::wstring_view familyName, - DWRITE_FONT_WEIGHT weight, + unsigned int weight, DWRITE_FONT_STYLE style, DWRITE_FONT_STRETCH stretch); DxFontInfo(std::wstring_view familyName, - unsigned int weight, + DWRITE_FONT_WEIGHT weight, DWRITE_FONT_STYLE style, DWRITE_FONT_STRETCH stretch); - std::wstring GetFamilyName() const; + std::wstring_view GetFamilyName() const noexcept; void SetFamilyName(const std::wstring_view familyName); DWRITE_FONT_WEIGHT GetWeight() const noexcept; From 04bbcda3bc04de35a0ab865e073a38d7c55e60e3 Mon Sep 17 00:00:00 2001 From: skyline75489 Date: Fri, 16 Apr 2021 14:54:24 +0800 Subject: [PATCH 05/11] [skip ci] private method naming --- src/renderer/dx/DxFontInfo.cpp | 4 ++-- src/renderer/dx/DxFontInfo.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/dx/DxFontInfo.cpp b/src/renderer/dx/DxFontInfo.cpp index 08ff8e7b368..14cc2248fb6 100644 --- a/src/renderer/dx/DxFontInfo.cpp +++ b/src/renderer/dx/DxFontInfo.cpp @@ -200,7 +200,7 @@ void DxFontInfo::SetFromEngine(const std::wstring_view familyName, // If the system collection missed, try the files sitting next to our binary. if (!familyExists) { - auto&& nearbyCollection = NearbyCollection(dwriteFactory); + auto&& nearbyCollection = _NearbyCollection(dwriteFactory); // May be null on OS below Windows 10. If null, just skip the attempt. if (nearbyCollection) @@ -309,7 +309,7 @@ void DxFontInfo::SetFromEngine(const std::wstring_view familyName, // - dwriteFactory - The DWrite factory to use // Return Value: // - DirectWrite font collection. May be null if one cannot be created. -[[nodiscard]] const Microsoft::WRL::ComPtr& DxFontInfo::NearbyCollection(gsl::not_null dwriteFactory) const +[[nodiscard]] const Microsoft::WRL::ComPtr& DxFontInfo::_NearbyCollection(gsl::not_null dwriteFactory) const { // Magic static so we only attempt to grovel the hard disk once no matter how many instances // of the font collection itself we require. diff --git a/src/renderer/dx/DxFontInfo.h b/src/renderer/dx/DxFontInfo.h index 676bc99ea5e..16ce5a39d7f 100644 --- a/src/renderer/dx/DxFontInfo.h +++ b/src/renderer/dx/DxFontInfo.h @@ -53,7 +53,7 @@ namespace Microsoft::Console::Render [[nodiscard]] std::wstring _GetFontFamilyName(gsl::not_null const fontFamily, std::wstring& localeName); - [[nodiscard]] const Microsoft::WRL::ComPtr& NearbyCollection(gsl::not_null dwriteFactory) const; + [[nodiscard]] const Microsoft::WRL::ComPtr& _NearbyCollection(gsl::not_null dwriteFactory) const; [[nodiscard]] static std::vector s_GetNearbyFonts(); From f381934a3bdfc0e56582f835003bfefbb475a9c3 Mon Sep 17 00:00:00 2001 From: Chester Liu Date: Thu, 6 May 2021 13:01:12 +0800 Subject: [PATCH 06/11] Refactor --- src/renderer/dx/CustomTextLayout.cpp | 14 +++- src/renderer/dx/DxFontInfo.cpp | 9 +++ src/renderer/dx/DxFontInfo.h | 21 ++++++ src/renderer/dx/DxFontRenderData.cpp | 106 ++++++++++++++++----------- src/renderer/dx/DxFontRenderData.h | 28 +++++-- 5 files changed, 125 insertions(+), 53 deletions(-) diff --git a/src/renderer/dx/CustomTextLayout.cpp b/src/renderer/dx/CustomTextLayout.cpp index 9dcbf9925de..d916be3b5cb 100644 --- a/src/renderer/dx/CustomTextLayout.cpp +++ b/src/renderer/dx/CustomTextLayout.cpp @@ -132,8 +132,18 @@ CATCH_RETURN() FLOAT originY) { const auto drawingContext = static_cast(clientDrawingContext); - _formatInUse = drawingContext->useItalicFont ? _fontRenderData->ItalicTextFormat().Get() : _fontRenderData->DefaultTextFormat().Get(); - _fontInUse = drawingContext->useItalicFont ? _fontRenderData->ItalicFontFace().Get() : _fontRenderData->DefaultFontFace().Get(); + + DWRITE_FONT_WEIGHT weight = _fontRenderData->DefaultFontWeight(); + DWRITE_FONT_STYLE style = _fontRenderData->DefaultFontStyle(); + DWRITE_FONT_STRETCH stretch = _fontRenderData->DefaultFontStretch(); + + if (drawingContext->useItalicFont) + { + style = DWRITE_FONT_STYLE_ITALIC; + } + + _formatInUse = _fontRenderData->TextFormatWithAttribute(weight, style, stretch).Get(); + _fontInUse = _fontRenderData->FontFaceWithAttribute(weight, style, stretch).Get(); RETURN_IF_FAILED(_AnalyzeRuns()); RETURN_IF_FAILED(_ShapeGlyphRuns()); diff --git a/src/renderer/dx/DxFontInfo.cpp b/src/renderer/dx/DxFontInfo.cpp index 14cc2248fb6..ee4699354b0 100644 --- a/src/renderer/dx/DxFontInfo.cpp +++ b/src/renderer/dx/DxFontInfo.cpp @@ -42,6 +42,15 @@ DxFontInfo::DxFontInfo(std::wstring_view familyName, { } +bool DxFontInfo::operator==(const DxFontInfo& other) const noexcept +{ + return (_familyName == other._familyName && + _weight == other._weight && + _style == other._style && + _stretch == other._stretch && + _didFallback == other._didFallback); +} + std::wstring_view DxFontInfo::GetFamilyName() const noexcept { return _familyName; diff --git a/src/renderer/dx/DxFontInfo.h b/src/renderer/dx/DxFontInfo.h index 16ce5a39d7f..edd377edf91 100644 --- a/src/renderer/dx/DxFontInfo.h +++ b/src/renderer/dx/DxFontInfo.h @@ -25,6 +25,8 @@ namespace Microsoft::Console::Render DWRITE_FONT_STYLE style, DWRITE_FONT_STRETCH stretch); + bool operator==(const DxFontInfo& other) const noexcept; + std::wstring_view GetFamilyName() const noexcept; void SetFamilyName(const std::wstring_view familyName); @@ -74,4 +76,23 @@ namespace Microsoft::Console::Render // Indicates whether we couldn't match the user request and had to choose from a hardcoded default list. bool _didFallback; }; + + struct DxFontInfoHash + { + size_t operator()(const DxFontInfo& fontInfo) const noexcept + { + size_t h1 = std::hash{}(fontInfo.GetFamilyName()); + size_t h2 = std::hash{}(fontInfo.GetWeight()); + size_t h3 = std::hash{}(fontInfo.GetStyle()); + size_t h4 = std::hash{}(fontInfo.GetStretch()); + size_t h5 = std::hash{}(fontInfo.GetFallback()); + + auto combine = [](size_t seed, size_t v) { + seed ^= v + 0x9e3779b9 + (seed << 6) + (seed >> 2); + return seed; + }; + + return combine(combine(combine(combine(h1, h2), h3), h4), h5); + } + }; } diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp index ccc8adb8fb4..56bc747ccf5 100644 --- a/src/renderer/dx/DxFontRenderData.cpp +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -78,34 +78,29 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr return _lineMetrics; } +[[nodiscard]] DWRITE_FONT_WEIGHT DxFontRenderData::DefaultFontWeight() noexcept +{ + return _defaultFontInfo.GetWeight(); +} + +[[nodiscard]] DWRITE_FONT_STYLE DxFontRenderData::DefaultFontStyle() noexcept +{ + return _defaultFontInfo.GetStyle(); +} + +[[nodiscard]] DWRITE_FONT_STRETCH DxFontRenderData::DefaultFontStretch() noexcept +{ + return _defaultFontInfo.GetStretch(); +} + [[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultTextFormat() { - if (!_dwriteTextFormat) - { - // Create the font with the fractional pixel height size. - // It should have an integer pixel width by our math. - // 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. - std::wstring localeName = UserLocaleName(); - THROW_IF_FAILED(_BuildTextFormat(_defaultFontInfo, localeName).As(&_dwriteTextFormat)); - 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)); - } - return _dwriteTextFormat; + return TextFormatWithAttribute(_defaultFontInfo.GetWeight(), _defaultFontInfo.GetStyle(), _defaultFontInfo.GetStretch()); } [[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultFontFace() { - if (!_dwriteFontFace) - { - std::wstring fontLocaleName = UserLocaleName(); - // _ResolveFontFaceWithFallback overrides the last argument with the locale name of the font, - // but we should use the system's locale to render the text. - _dwriteFontFace = _defaultFontInfo.ResolveFontFaceWithFallback(_dwriteFactory.Get(), fontLocaleName); - } - - return _dwriteFontFace; + return FontFaceWithAttribute(_defaultFontInfo.GetWeight(), _defaultFontInfo.GetStyle(), _defaultFontInfo.GetStretch()); } [[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultBoxDrawingEffect() @@ -119,33 +114,60 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr return _boxDrawingEffect; } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::ItalicTextFormat() +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::TextFormatWithAttribute(DWRITE_FONT_WEIGHT weight, + DWRITE_FONT_STYLE style, + DWRITE_FONT_STRETCH stretch) { - if (!_dwriteTextFormatItalic) + DxFontInfo fontInfo = _defaultFontInfo; + fontInfo.SetWeight(weight); + fontInfo.SetStyle(style); + fontInfo.SetStretch(stretch); + + auto textFormatIt = _textFormatMap.find(fontInfo); + if (textFormatIt == _textFormatMap.end()) { + // Create the font with the fractional pixel height size. + // It should have an integer pixel width by our math. + // 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. std::wstring localeName = UserLocaleName(); - DxFontInfo fontInfoItalic = _defaultFontInfo; - fontInfoItalic.SetStyle(DWRITE_FONT_STYLE_ITALIC); - THROW_IF_FAILED(_BuildTextFormat(fontInfoItalic, localeName).As(&_dwriteTextFormatItalic)); - THROW_IF_FAILED(_dwriteTextFormatItalic->SetLineSpacing(_lineSpacing.method, _lineSpacing.height, _lineSpacing.baseline)); - THROW_IF_FAILED(_dwriteTextFormatItalic->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR)); - THROW_IF_FAILED(_dwriteTextFormatItalic->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP)); + Microsoft::WRL::ComPtr textFormat; + THROW_IF_FAILED(_BuildTextFormat(fontInfo, localeName).As(&textFormat)); + 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)); + + _textFormatMap.insert({ fontInfo, textFormat }); + return textFormat; + } + else + { + return (*textFormatIt).second; } - - return _dwriteTextFormatItalic; } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::ItalicFontFace() +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::FontFaceWithAttribute(DWRITE_FONT_WEIGHT weight, + DWRITE_FONT_STYLE style, + DWRITE_FONT_STRETCH stretch) { - if (!_dwriteFontFaceItalic) + DxFontInfo fontInfo = _defaultFontInfo; + fontInfo.SetWeight(weight); + fontInfo.SetStyle(style); + fontInfo.SetStretch(stretch); + + auto fontFaceIt = _fontFaceMap.find(fontInfo); + if (fontFaceIt == _fontFaceMap.end()) { std::wstring fontLocaleName = UserLocaleName(); - DxFontInfo fontInfoItalic = _defaultFontInfo; - fontInfoItalic.SetStyle(DWRITE_FONT_STYLE_ITALIC); - _dwriteFontFaceItalic = fontInfoItalic.ResolveFontFaceWithFallback(_dwriteFactory.Get(), fontLocaleName); - } + Microsoft::WRL::ComPtr fontFace = fontInfo.ResolveFontFaceWithFallback(_dwriteFactory.Get(), fontLocaleName); - return _dwriteFontFaceItalic; + _fontFaceMap.insert({ fontInfo, fontFace }); + return fontFace; + } + else + { + return (*fontFaceIt).second; + } } // Routine Description: @@ -161,11 +183,9 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr try { _userLocaleName.clear(); - _dwriteTextFormat.Reset(); - _dwriteFontFace.Reset(); + _textFormatMap.clear(); + _fontFaceMap.clear(); _boxDrawingEffect.Reset(); - _dwriteTextFormatItalic.Reset(); - _dwriteFontFaceItalic.Reset(); // Initialize the default font info and build everything from here. _defaultFontInfo = DxFontInfo(desired.GetFaceName(), diff --git a/src/renderer/dx/DxFontRenderData.h b/src/renderer/dx/DxFontRenderData.h index 63f4528ddc4..abb49bf1666 100644 --- a/src/renderer/dx/DxFontRenderData.h +++ b/src/renderer/dx/DxFontRenderData.h @@ -42,6 +42,15 @@ namespace Microsoft::Console::Render [[nodiscard]] til::size GlyphCell() noexcept; [[nodiscard]] LineMetrics GetLineMetrics() noexcept; + // The weight of default font + [[nodiscard]] DWRITE_FONT_WEIGHT DefaultFontWeight() noexcept; + + // The style of default font + [[nodiscard]] DWRITE_FONT_STYLE DefaultFontStyle() noexcept; + + // The stretch of default font + [[nodiscard]] DWRITE_FONT_STRETCH DefaultFontStretch() noexcept; + // The DirectWrite format object representing the size and other text properties to be applied (by default) [[nodiscard]] Microsoft::WRL::ComPtr DefaultTextFormat(); @@ -51,11 +60,15 @@ namespace Microsoft::Console::Render // Box drawing scaling effects that are cached for the base font across layouts [[nodiscard]] Microsoft::WRL::ComPtr DefaultBoxDrawingEffect(); - // The italic variant of the format object representing the size and other text properties for italic text - [[nodiscard]] Microsoft::WRL::ComPtr ItalicTextFormat(); + // The attributed variants of the format object representing the size and other text properties + [[nodiscard]] Microsoft::WRL::ComPtr TextFormatWithAttribute(DWRITE_FONT_WEIGHT weight, + DWRITE_FONT_STYLE style, + DWRITE_FONT_STRETCH stretch); - // The italic variant of the font face to use while calculating layout for italic text - [[nodiscard]] Microsoft::WRL::ComPtr ItalicFontFace(); + // The attributed variants of the font face to use while calculating layout + [[nodiscard]] Microsoft::WRL::ComPtr FontFaceWithAttribute(DWRITE_FONT_WEIGHT weight, + DWRITE_FONT_STYLE style, + DWRITE_FONT_STRETCH stretch); [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& desired, FontInfo& fiFontInfo, const int dpi) noexcept; @@ -68,10 +81,9 @@ namespace Microsoft::Console::Render ::Microsoft::WRL::ComPtr _dwriteFactory; ::Microsoft::WRL::ComPtr _dwriteTextAnalyzer; - ::Microsoft::WRL::ComPtr _dwriteTextFormat; - ::Microsoft::WRL::ComPtr _dwriteTextFormatItalic; - ::Microsoft::WRL::ComPtr _dwriteFontFace; - ::Microsoft::WRL::ComPtr _dwriteFontFaceItalic; + + std::unordered_map, DxFontInfoHash> _textFormatMap; + std::unordered_map, DxFontInfoHash> _fontFaceMap; ::Microsoft::WRL::ComPtr _boxDrawingEffect; From eb581a4db71be2da3c614b86150ae7db8f28319f Mon Sep 17 00:00:00 2001 From: Chester Liu Date: Thu, 6 May 2021 13:28:01 +0800 Subject: [PATCH 07/11] Audit --- src/renderer/dx/CustomTextLayout.cpp | 8 +++++--- src/renderer/dx/CustomTextLayout.h | 2 +- src/renderer/dx/DxFontInfo.h | 12 ++++++------ src/renderer/dx/DxFontRenderData.cpp | 4 ++-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/renderer/dx/CustomTextLayout.cpp b/src/renderer/dx/CustomTextLayout.cpp index d916be3b5cb..f1b9466c6be 100644 --- a/src/renderer/dx/CustomTextLayout.cpp +++ b/src/renderer/dx/CustomTextLayout.cpp @@ -129,13 +129,14 @@ CATCH_RETURN() [[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::Draw(_In_opt_ void* clientDrawingContext, _In_ IDWriteTextRenderer* renderer, FLOAT originX, - FLOAT originY) + FLOAT originY) noexcept +try { const auto drawingContext = static_cast(clientDrawingContext); - DWRITE_FONT_WEIGHT weight = _fontRenderData->DefaultFontWeight(); + const DWRITE_FONT_WEIGHT weight = _fontRenderData->DefaultFontWeight(); DWRITE_FONT_STYLE style = _fontRenderData->DefaultFontStyle(); - DWRITE_FONT_STRETCH stretch = _fontRenderData->DefaultFontStretch(); + const DWRITE_FONT_STRETCH stretch = _fontRenderData->DefaultFontStretch(); if (drawingContext->useItalicFont) { @@ -157,6 +158,7 @@ CATCH_RETURN() return S_OK; } +CATCH_RETURN() // Routine Description: // - Uses the internal text information and the analyzers/font information from construction diff --git a/src/renderer/dx/CustomTextLayout.h b/src/renderer/dx/CustomTextLayout.h index 52bfc99e289..51f119bb0d8 100644 --- a/src/renderer/dx/CustomTextLayout.h +++ b/src/renderer/dx/CustomTextLayout.h @@ -33,7 +33,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT STDMETHODCALLTYPE Draw(_In_opt_ void* clientDrawingContext, _In_ IDWriteTextRenderer* renderer, FLOAT originX, - FLOAT originY); + FLOAT originY) noexcept; // IDWriteTextAnalysisSource methods [[nodiscard]] HRESULT STDMETHODCALLTYPE GetTextAtPosition(UINT32 textPosition, diff --git a/src/renderer/dx/DxFontInfo.h b/src/renderer/dx/DxFontInfo.h index edd377edf91..8d12d818ead 100644 --- a/src/renderer/dx/DxFontInfo.h +++ b/src/renderer/dx/DxFontInfo.h @@ -81,13 +81,13 @@ namespace Microsoft::Console::Render { size_t operator()(const DxFontInfo& fontInfo) const noexcept { - size_t h1 = std::hash{}(fontInfo.GetFamilyName()); - size_t h2 = std::hash{}(fontInfo.GetWeight()); - size_t h3 = std::hash{}(fontInfo.GetStyle()); - size_t h4 = std::hash{}(fontInfo.GetStretch()); - size_t h5 = std::hash{}(fontInfo.GetFallback()); + const size_t h1 = std::hash{}(fontInfo.GetFamilyName()); + const size_t h2 = std::hash{}(fontInfo.GetWeight()); + const size_t h3 = std::hash{}(fontInfo.GetStyle()); + const size_t h4 = std::hash{}(fontInfo.GetStretch()); + const size_t h5 = std::hash{}(fontInfo.GetFallback()); - auto combine = [](size_t seed, size_t v) { + const auto combine = [](size_t seed, size_t v) { seed ^= v + 0x9e3779b9 + (seed << 6) + (seed >> 2); return seed; }; diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp index 56bc747ccf5..b2c3e2ef3f1 100644 --- a/src/renderer/dx/DxFontRenderData.cpp +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -123,7 +123,7 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr fontInfo.SetStyle(style); fontInfo.SetStretch(stretch); - auto textFormatIt = _textFormatMap.find(fontInfo); + const auto textFormatIt = _textFormatMap.find(fontInfo); if (textFormatIt == _textFormatMap.end()) { // Create the font with the fractional pixel height size. @@ -155,7 +155,7 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr fontInfo.SetStyle(style); fontInfo.SetStretch(stretch); - auto fontFaceIt = _fontFaceMap.find(fontInfo); + const auto fontFaceIt = _fontFaceMap.find(fontInfo); if (fontFaceIt == _fontFaceMap.end()) { std::wstring fontLocaleName = UserLocaleName(); From c4ab9b5ee5287d220f40812038c2e1dc39ee1d79 Mon Sep 17 00:00:00 2001 From: Chester Liu Date: Mon, 7 Jun 2021 15:13:54 +0800 Subject: [PATCH 08/11] Fix merge --- src/renderer/dx/DxFontInfo.cpp | 108 +++++++++++++++++++-------------- src/renderer/dx/DxFontInfo.h | 4 +- 2 files changed, 64 insertions(+), 48 deletions(-) diff --git a/src/renderer/dx/DxFontInfo.cpp b/src/renderer/dx/DxFontInfo.cpp index ee4699354b0..03c224868f4 100644 --- a/src/renderer/dx/DxFontInfo.cpp +++ b/src/renderer/dx/DxFontInfo.cpp @@ -122,63 +122,77 @@ void DxFontInfo::SetFromEngine(const std::wstring_view familyName, { // First attempt to find exactly what the user asked for. _didFallback = false; - auto face = _FindFontFace(dwriteFactory, localeName); + Microsoft::WRL::ComPtr face{ nullptr }; - if (!face) + // GH#10211 - wrap this all up in a try/catch. If the nearby fonts are + // corrupted, then we don't want to throw out of this top half of this + // method. We still want to fall back to a font that's reasonable, below. + try { - // If we missed, try looking a little more by trimming the last word off the requested family name a few times. - // Quite often, folks are specifying weights or something in the familyName and it causes failed resolution and - // an unexpected error dialog. We theoretically could detect the weight words and convert them, but this - // is the quick fix for the majority scenario. - // The long/full fix is backlogged to GH#9744 - // Also this doesn't count as a fallback because we don't want to annoy folks with the warning dialog over - // this resolution. - while (!face && !_familyName.empty()) - { - const auto lastSpace = _familyName.find_last_of(UNICODE_SPACE); - - // value is unsigned and npos will be greater than size. - // if we didn't find anything to trim, leave. - if (lastSpace >= _familyName.size()) - { - break; - } - - // trim string down to just before the found space - // (space found at 6... trim from 0 for 6 length will give us 0-5 as the new string) - _familyName = _familyName.substr(0, lastSpace); + face = _FindFontFace(dwriteFactory, localeName, true); - // Try to find it with the shortened family name - face = _FindFontFace(dwriteFactory, localeName); - } - - // Alright, if our quick shot at trimming didn't work either... - // move onto looking up a font from our hardcoded list of fonts - // that should really always be available. if (!face) { - for (const auto fallbackFace : FALLBACK_FONT_FACES) + // If we missed, try looking a little more by trimming the last word off the requested family name a few times. + // Quite often, folks are specifying weights or something in the familyName and it causes failed resolution and + // an unexpected error dialog. We theoretically could detect the weight words and convert them, but this + // is the quick fix for the majority scenario. + // The long/full fix is backlogged to GH#9744 + // Also this doesn't count as a fallback because we don't want to annoy folks with the warning dialog over + // this resolution. + while (!face && !_familyName.empty()) { - _familyName = fallbackFace; - face = _FindFontFace(dwriteFactory, localeName); + const auto lastSpace = _familyName.find_last_of(UNICODE_SPACE); - if (face) + // value is unsigned and npos will be greater than size. + // if we didn't find anything to trim, leave. + if (lastSpace >= _familyName.size()) { - _didFallback = true; break; } - _familyName = fallbackFace; - _weight = DWRITE_FONT_WEIGHT_NORMAL; - _stretch = DWRITE_FONT_STRETCH_NORMAL; - _style = DWRITE_FONT_STYLE_NORMAL; - face = _FindFontFace(dwriteFactory, localeName); + // trim string down to just before the found space + // (space found at 6... trim from 0 for 6 length will give us 0-5 as the new string) + _familyName = _familyName.substr(0, lastSpace); - if (face) - { - _didFallback = true; - break; - } + // Try to find it with the shortened family name + face = _FindFontFace(dwriteFactory, localeName, true); + } + } + } + CATCH_LOG(); + + // Alright, if our quick shot at trimming didn't work either... + // move onto looking up a font from our hardcoded list of fonts + // that should really always be available. + if (!face) + { + for (const auto fallbackFace : FALLBACK_FONT_FACES) + { + _familyName = fallbackFace; + // With these fonts, don't attempt the nearby lookup. We're looking + // for system fonts only. If one of the nearby fonts is causing us + // problems (like in GH#10211), then we don't want to go anywhere + + // near it in this part. + face = _FindFontFace(dwriteFactory, localeName, false); + + if (face) + { + _didFallback = true; + break; + } + + _familyName = fallbackFace; + _weight = DWRITE_FONT_WEIGHT_NORMAL; + _stretch = DWRITE_FONT_STRETCH_NORMAL; + _style = DWRITE_FONT_STYLE_NORMAL; + face = _FindFontFace(dwriteFactory, localeName, false); + + if (face) + { + _didFallback = true; + break; } } } @@ -195,7 +209,7 @@ void DxFontInfo::SetFromEngine(const std::wstring_view familyName, // - localeName - Locale to search for appropriate fonts // Return Value: // - Smart pointer holding interface reference for queryable font data. -[[nodiscard]] Microsoft::WRL::ComPtr DxFontInfo::_FindFontFace(gsl::not_null dwriteFactory, std::wstring& localeName) +[[nodiscard]] Microsoft::WRL::ComPtr DxFontInfo::_FindFontFace(gsl::not_null dwriteFactory, std::wstring& localeName, const bool withNearbyLookup) { Microsoft::WRL::ComPtr fontFace; @@ -207,7 +221,7 @@ void DxFontInfo::SetFromEngine(const std::wstring_view familyName, THROW_IF_FAILED(fontCollection->FindFamilyName(_familyName.data(), &familyIndex, &familyExists)); // If the system collection missed, try the files sitting next to our binary. - if (!familyExists) + if (withNearbyLookup && !familyExists) { auto&& nearbyCollection = _NearbyCollection(dwriteFactory); diff --git a/src/renderer/dx/DxFontInfo.h b/src/renderer/dx/DxFontInfo.h index 8d12d818ead..cb1b89b41ea 100644 --- a/src/renderer/dx/DxFontInfo.h +++ b/src/renderer/dx/DxFontInfo.h @@ -50,7 +50,9 @@ namespace Microsoft::Console::Render std::wstring& localeName); private: - [[nodiscard]] ::Microsoft::WRL::ComPtr _FindFontFace(gsl::not_null dwriteFactory, std::wstring& localeName); + [[nodiscard]] ::Microsoft::WRL::ComPtr _FindFontFace(gsl::not_null dwriteFactory, + std::wstring& localeName, + const bool withNearbyLookup); [[nodiscard]] std::wstring _GetFontFamilyName(gsl::not_null const fontFamily, std::wstring& localeName); From 26a489b2c64f7a9c70d97def8cbcdebdfee170de Mon Sep 17 00:00:00 2001 From: Chester Liu Date: Fri, 18 Jun 2021 18:55:42 +0800 Subject: [PATCH 09/11] Fix ControlUnitTests --- src/cascadia/UnitTests_Control/MockControlSettings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cascadia/UnitTests_Control/MockControlSettings.h b/src/cascadia/UnitTests_Control/MockControlSettings.h index f3c1930189f..d204513c2f7 100644 --- a/src/cascadia/UnitTests_Control/MockControlSettings.h +++ b/src/cascadia/UnitTests_Control/MockControlSettings.h @@ -52,7 +52,7 @@ namespace ControlUnitTests WINRT_PROPERTY(winrt::hstring, FontFace, L"Consolas"); WINRT_PROPERTY(int32_t, FontSize, DEFAULT_FONT_SIZE); - WINRT_PROPERTY(winrt::Windows::UI::Text::FontWeight, FontWeight); + WINRT_PROPERTY(winrt::Windows::UI::Text::FontWeight, FontWeight, 400); WINRT_PROPERTY(winrt::hstring, BackgroundImage); WINRT_PROPERTY(double, BackgroundImageOpacity, 1.0); From a59757a2ce6e4fe694253e7dc8fba22fdfd90c56 Mon Sep 17 00:00:00 2001 From: Chester Liu Date: Fri, 18 Jun 2021 19:40:56 +0800 Subject: [PATCH 10/11] Handle first font resolve attempt correctly --- src/cascadia/UnitTests_Control/MockControlSettings.h | 2 +- src/renderer/dx/DxFontRenderData.cpp | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/cascadia/UnitTests_Control/MockControlSettings.h b/src/cascadia/UnitTests_Control/MockControlSettings.h index d204513c2f7..f3c1930189f 100644 --- a/src/cascadia/UnitTests_Control/MockControlSettings.h +++ b/src/cascadia/UnitTests_Control/MockControlSettings.h @@ -52,7 +52,7 @@ namespace ControlUnitTests WINRT_PROPERTY(winrt::hstring, FontFace, L"Consolas"); WINRT_PROPERTY(int32_t, FontSize, DEFAULT_FONT_SIZE); - WINRT_PROPERTY(winrt::Windows::UI::Text::FontWeight, FontWeight, 400); + WINRT_PROPERTY(winrt::Windows::UI::Text::FontWeight, FontWeight); WINRT_PROPERTY(winrt::hstring, BackgroundImage); WINRT_PROPERTY(double, BackgroundImageOpacity, 1.0); diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp index b2c3e2ef3f1..23196f57828 100644 --- a/src/renderer/dx/DxFontRenderData.cpp +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -451,7 +451,10 @@ CATCH_RETURN() // - None void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi) { - const auto face = DefaultFontFace(); + std::wstring fontLocaleName = UserLocaleName(); + // This is the first attempt to resolve font face after 'UpdateFont'. + // Note that the following line may cause changes of the font properties in _defaultFontInfo. + const Microsoft::WRL::ComPtr face = _defaultFontInfo.ResolveFontFaceWithFallback(_dwriteFactory.Get(), fontLocaleName); DWRITE_FONT_METRICS1 fontMetrics; face->GetMetrics(&fontMetrics); From 806eef4c0a24db7359fd8ca4f0e46299904b1d11 Mon Sep 17 00:00:00 2001 From: Chester Liu Date: Tue, 22 Jun 2021 13:24:37 +0800 Subject: [PATCH 11/11] Feedback --- src/renderer/dx/DxFontInfo.h | 18 +++++++++++++----- src/renderer/dx/DxFontRenderData.cpp | 5 +++-- src/renderer/dx/DxFontRenderData.h | 4 ++-- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/renderer/dx/DxFontInfo.h b/src/renderer/dx/DxFontInfo.h index cb1b89b41ea..924202632ee 100644 --- a/src/renderer/dx/DxFontInfo.h +++ b/src/renderer/dx/DxFontInfo.h @@ -78,10 +78,14 @@ namespace Microsoft::Console::Render // Indicates whether we couldn't match the user request and had to choose from a hardcoded default list. bool _didFallback; }; +} - struct DxFontInfoHash +namespace std +{ + template<> + struct hash { - size_t operator()(const DxFontInfo& fontInfo) const noexcept + size_t operator()(const Microsoft::Console::Render::DxFontInfo& fontInfo) const noexcept { const size_t h1 = std::hash{}(fontInfo.GetFamilyName()); const size_t h2 = std::hash{}(fontInfo.GetWeight()); @@ -89,12 +93,16 @@ namespace Microsoft::Console::Render const size_t h4 = std::hash{}(fontInfo.GetStretch()); const size_t h5 = std::hash{}(fontInfo.GetFallback()); - const auto combine = [](size_t seed, size_t v) { - seed ^= v + 0x9e3779b9 + (seed << 6) + (seed >> 2); + static const auto combine = [](std::initializer_list list) { + size_t seed = 0; + for (auto hash : list) + { + seed ^= hash + 0x9e3779b9 + (seed << 6) + (seed >> 2); + } return seed; }; - return combine(combine(combine(combine(h1, h2), h3), h4), h5); + return combine({ h1, h2, h3, h4, h5 }); } }; } diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp index 23196f57828..20340ba6dc1 100644 --- a/src/renderer/dx/DxFontRenderData.cpp +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -452,8 +452,9 @@ CATCH_RETURN() void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi) { std::wstring fontLocaleName = UserLocaleName(); - // This is the first attempt to resolve font face after 'UpdateFont'. - // Note that the following line may cause changes of the font properties in _defaultFontInfo. + // This is the first attempt to resolve font face after `UpdateFont`. + // Note that the following line may cause property changes _inside_ `_defaultFontInfo` because the desired font may not exist. + // See the implementation of `ResolveFontFaceWithFallback` for details. const Microsoft::WRL::ComPtr face = _defaultFontInfo.ResolveFontFaceWithFallback(_dwriteFactory.Get(), fontLocaleName); DWRITE_FONT_METRICS1 fontMetrics; diff --git a/src/renderer/dx/DxFontRenderData.h b/src/renderer/dx/DxFontRenderData.h index abb49bf1666..990bcbd966e 100644 --- a/src/renderer/dx/DxFontRenderData.h +++ b/src/renderer/dx/DxFontRenderData.h @@ -82,8 +82,8 @@ namespace Microsoft::Console::Render ::Microsoft::WRL::ComPtr _dwriteTextAnalyzer; - std::unordered_map, DxFontInfoHash> _textFormatMap; - std::unordered_map, DxFontInfoHash> _fontFaceMap; + std::unordered_map> _textFormatMap; + std::unordered_map> _fontFaceMap; ::Microsoft::WRL::ComPtr _boxDrawingEffect;