From 85c485e94fd067ab6541c1b8c7f6d61b33099d58 Mon Sep 17 00:00:00 2001 From: Chester Liu Date: Wed, 23 Jun 2021 03:31:27 +0800 Subject: [PATCH] Introduce DxFontInfo (#9201) This PR Introduces `DxFontInfo` to simplify the logic in `DxFontRenderData`. `DxFontInfo` aims to be the DWrite equivalent of `FontInfo` & `FontInfoBase` in GDI. It encapsulates the needed information to represent a displayable font face. It also provides the ability to resolve a font face based on the available fonts on the system. ## References This is a follow-up of #9096. Initial Italic support was introduced by #8580. The motivation behind this is to support bold & bold-italic text in Windows Terminal. --- src/renderer/dx/CustomTextLayout.cpp | 16 +- src/renderer/dx/DxFontInfo.cpp | 405 +++++++++++++ src/renderer/dx/DxFontInfo.h | 108 ++++ src/renderer/dx/DxFontRenderData.cpp | 860 ++++++++++----------------- src/renderer/dx/DxFontRenderData.h | 67 +-- src/renderer/dx/lib/dx.vcxproj | 2 + src/renderer/dx/sources.inc | 1 + 7 files changed, 864 insertions(+), 595 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..0e25d96a27c 100644 --- a/src/renderer/dx/CustomTextLayout.cpp +++ b/src/renderer/dx/CustomTextLayout.cpp @@ -133,10 +133,21 @@ 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(); - _fontInUse = drawingContext->useItalicFont ? _fontRenderData->ItalicFontFace().Get() : _fontRenderData->DefaultFontFace().Get(); + + const DWRITE_FONT_WEIGHT weight = _fontRenderData->DefaultFontWeight(); + DWRITE_FONT_STYLE style = _fontRenderData->DefaultFontStyle(); + const 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(_AnalyzeTextComplexity()); RETURN_IF_FAILED(_AnalyzeRuns()); @@ -151,6 +162,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..03c224868f4 --- /dev/null +++ b/src/renderer/dx/DxFontInfo.cpp @@ -0,0 +1,405 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "DxFontInfo.h" + +#include "unicode.hpp" + +#include + +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), + _didFallback(false) +{ +} + +DxFontInfo::DxFontInfo(std::wstring_view familyName, + unsigned int weight, + DWRITE_FONT_STYLE style, + DWRITE_FONT_STRETCH stretch) : + DxFontInfo(familyName, static_cast(weight), style, stretch) +{ +} + +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), + _didFallback(false) +{ +} + +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; +} + +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; +} + +bool DxFontInfo::GetFallback() const noexcept +{ + return _didFallback; +} + +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: +// - 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, + std::wstring& localeName) +{ + // First attempt to find exactly what the user asked for. + _didFallback = false; + Microsoft::WRL::ComPtr face{ nullptr }; + + // 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 + { + face = _FindFontFace(dwriteFactory, localeName, true); + + if (!face) + { + // 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); + + // 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; + } + } + } + + THROW_HR_IF_NULL(E_FAIL, face); + + return face; +} + +// Routine Description: +// - Locates a suitable font face from the given information +// Arguments: +// - 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, const bool withNearbyLookup) +{ + Microsoft::WRL::ComPtr fontFace; + + Microsoft::WRL::ComPtr fontCollection; + THROW_IF_FAILED(dwriteFactory->GetSystemFontCollection(&fontCollection, false)); + + UINT32 familyIndex; + BOOL familyExists; + THROW_IF_FAILED(fontCollection->FindFamilyName(_familyName.data(), &familyIndex, &familyExists)); + + // If the system collection missed, try the files sitting next to our binary. + if (withNearbyLookup && !familyExists) + { + auto&& nearbyCollection = _NearbyCollection(dwriteFactory); + + // May be null on OS below Windows 10. If null, just skip the attempt. + if (nearbyCollection) + { + nearbyCollection.As(&fontCollection); + THROW_IF_FAILED(fontCollection->FindFamilyName(_familyName.data(), &familyIndex, &familyExists)); + } + } + + if (familyExists) + { + Microsoft::WRL::ComPtr fontFamily; + THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily)); + + Microsoft::WRL::ComPtr font; + THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(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. + _weight = font->GetWeight(); + _stretch = font->GetStretch(); + _style = font->GetStyle(); + + // Dig the family name out at the end to return it. + _familyName = _GetFontFamilyName(fontFamily.Get(), localeName); + } + + return fontFace; +} + +// Routine Description: +// - Retrieves the font family name out of the given object in the given locale. +// - If we can't find a valid name for the given locale, we'll fallback and report it back. +// Arguments: +// - fontFamily - DirectWrite font family object +// - localeName - The locale in which the name should be retrieved. +// - If fallback occurred, this is updated to what we retrieved instead. +// Return Value: +// - Localized string name of the font family +[[nodiscard]] std::wstring 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; +} + +// Routine Description: +// - 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 +{ + // Magic static so we only attempt to grovel the hard disk once no matter how many instances + // of the font collection itself we require. + static const auto knownPaths = s_GetNearbyFonts(); + + // The convenience interfaces for loading fonts from files + // are only available on Windows 10+. + // Don't try to look up if below that OS version. + static const bool s_isWindows10OrGreater = IsWindows10OrGreater(); + + if (s_isWindows10OrGreater && !_nearbyCollection) + { + // Factory3 has a convenience to get us a font set builder. + ::Microsoft::WRL::ComPtr factory3; + THROW_IF_FAILED(dwriteFactory->QueryInterface(&factory3)); + + ::Microsoft::WRL::ComPtr fontSetBuilder; + THROW_IF_FAILED(factory3->CreateFontSetBuilder(&fontSetBuilder)); + + // Builder2 has a convenience to just feed in paths to font files. + ::Microsoft::WRL::ComPtr fontSetBuilder2; + THROW_IF_FAILED(fontSetBuilder.As(&fontSetBuilder2)); + + for (auto& p : knownPaths) + { + fontSetBuilder2->AddFontFile(p.c_str()); + } + + ::Microsoft::WRL::ComPtr fontSet; + THROW_IF_FAILED(fontSetBuilder2->CreateFontSet(&fontSet)); + + THROW_IF_FAILED(factory3->CreateFontCollectionFromFontSet(fontSet.Get(), &_nearbyCollection)); + } + + 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; + + // 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/DxFontInfo.h b/src/renderer/dx/DxFontInfo.h new file mode 100644 index 00000000000..924202632ee --- /dev/null +++ b/src/renderer/dx/DxFontInfo.h @@ -0,0 +1,108 @@ +// 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, + unsigned int weight, + DWRITE_FONT_STYLE style, + DWRITE_FONT_STRETCH stretch); + + DxFontInfo(std::wstring_view familyName, + DWRITE_FONT_WEIGHT weight, + 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); + + 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; + + bool GetFallback() const 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, + const bool withNearbyLookup); + + [[nodiscard]] std::wstring _GetFontFamilyName(gsl::not_null const fontFamily, + std::wstring& localeName); + + [[nodiscard]] const Microsoft::WRL::ComPtr& _NearbyCollection(gsl::not_null dwriteFactory) const; + + [[nodiscard]] static std::vector s_GetNearbyFonts(); + + mutable ::Microsoft::WRL::ComPtr _nearbyCollection; + + // The font name we should be looking for + std::wstring _familyName; + + // The weight (bold, light, etc.) + DWRITE_FONT_WEIGHT _weight; + + // Normal, italic, etc. + DWRITE_FONT_STYLE _style; + + // The stretch of the font is the spacing between each letter + DWRITE_FONT_STRETCH _stretch; + + // Indicates whether we couldn't match the user request and had to choose from a hardcoded default list. + bool _didFallback; + }; +} + +namespace std +{ + template<> + struct hash + { + 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()); + const size_t h3 = std::hash{}(fontInfo.GetStyle()); + const size_t h4 = std::hash{}(fontInfo.GetStretch()); + const size_t h5 = std::hash{}(fontInfo.GetFallback()); + + 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({ h1, h2, h3, h4, h5 }); + } + }; +} diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp index d9360525e3c..20340ba6dc1 100644 --- a/src/renderer/dx/DxFontRenderData.cpp +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -17,14 +17,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; } @@ -40,49 +48,24 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr return _systemFontFallback; } -// Routine Description: -// - Creates a DirectWrite font collection of font files that are sitting next to the running -// binary (in the same directory as the EXE). -// Arguments: -// - -// Return Value: -// - DirectWrite font collection. May be null if one cannot be created. -[[nodiscard]] const Microsoft::WRL::ComPtr& DxFontRenderData::NearbyCollection() const +[[nodiscard]] std::wstring DxFontRenderData::UserLocaleName() { - // Magic static so we only attempt to grovel the hard disk once no matter how many instances - // of the font collection itself we require. - static const auto knownPaths = s_GetNearbyFonts(); - - // The convenience interfaces for loading fonts from files - // are only available on Windows 10+. - // Don't try to look up if below that OS version. - static const bool s_isWindows10OrGreater = IsWindows10OrGreater(); - - if (s_isWindows10OrGreater && !_nearbyCollection) + if (_userLocaleName.empty()) { - // Factory3 has a convenience to get us a font set builder. - ::Microsoft::WRL::ComPtr factory3; - THROW_IF_FAILED(_dwriteFactory.As(&factory3)); - - ::Microsoft::WRL::ComPtr fontSetBuilder; - THROW_IF_FAILED(factory3->CreateFontSetBuilder(&fontSetBuilder)); - - // Builder2 has a convenience to just feed in paths to font files. - ::Microsoft::WRL::ComPtr fontSetBuilder2; - THROW_IF_FAILED(fontSetBuilder.As(&fontSetBuilder2)); + std::array localeName; - for (auto& p : knownPaths) + const auto returnCode = GetUserDefaultLocaleName(localeName.data(), gsl::narrow(localeName.size())); + if (returnCode) { - fontSetBuilder2->AddFontFile(p.c_str()); + _userLocaleName = { localeName.data() }; + } + else + { + _userLocaleName = { FALLBACK_LOCALE.data(), FALLBACK_LOCALE.size() }; } - - ::Microsoft::WRL::ComPtr fontSet; - THROW_IF_FAILED(fontSetBuilder2->CreateFontSet(&fontSet)); - - THROW_IF_FAILED(factory3->CreateFontCollectionFromFontSet(fontSet.Get(), &_nearbyCollection)); } - return _nearbyCollection; + return _userLocaleName; } [[nodiscard]] til::size DxFontRenderData::GlyphCell() noexcept @@ -95,29 +78,96 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr return _lineMetrics; } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultTextFormat() noexcept +[[nodiscard]] DWRITE_FONT_WEIGHT DxFontRenderData::DefaultFontWeight() noexcept { - return _dwriteTextFormat; + return _defaultFontInfo.GetWeight(); } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultFontFace() noexcept +[[nodiscard]] DWRITE_FONT_STYLE DxFontRenderData::DefaultFontStyle() noexcept { - return _dwriteFontFace; + return _defaultFontInfo.GetStyle(); } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultBoxDrawingEffect() noexcept +[[nodiscard]] DWRITE_FONT_STRETCH DxFontRenderData::DefaultFontStretch() noexcept { + return _defaultFontInfo.GetStretch(); +} + +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultTextFormat() +{ + return TextFormatWithAttribute(_defaultFontInfo.GetWeight(), _defaultFontInfo.GetStyle(), _defaultFontInfo.GetStretch()); +} + +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultFontFace() +{ + return FontFaceWithAttribute(_defaultFontInfo.GetWeight(), _defaultFontInfo.GetStyle(), _defaultFontInfo.GetStretch()); +} + +[[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::TextFormatWithAttribute(DWRITE_FONT_WEIGHT weight, + DWRITE_FONT_STYLE style, + DWRITE_FONT_STRETCH stretch) { - return _dwriteTextFormatItalic; + DxFontInfo fontInfo = _defaultFontInfo; + fontInfo.SetWeight(weight); + fontInfo.SetStyle(style); + fontInfo.SetStretch(stretch); + + const 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(); + 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; + } } -[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::ItalicFontFace() noexcept +[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::FontFaceWithAttribute(DWRITE_FONT_WEIGHT weight, + DWRITE_FONT_STYLE style, + DWRITE_FONT_STRETCH stretch) { - return _dwriteFontFaceItalic; + DxFontInfo fontInfo = _defaultFontInfo; + fontInfo.SetWeight(weight); + fontInfo.SetStyle(style); + fontInfo.SetStretch(stretch); + + const auto fontFaceIt = _fontFaceMap.find(fontInfo); + if (fontFaceIt == _fontFaceMap.end()) + { + std::wstring fontLocaleName = UserLocaleName(); + Microsoft::WRL::ComPtr fontFace = fontInfo.ResolveFontFaceWithFallback(_dwriteFactory.Get(), fontLocaleName); + + _fontFaceMap.insert({ fontInfo, fontFace }); + return fontFace; + } + else + { + return (*fontFaceIt).second; + } } // Routine Description: @@ -133,247 +183,17 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr try { _userLocaleName.clear(); + _textFormatMap.clear(); + _fontFaceMap.clear(); + _boxDrawingEffect.Reset(); - 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; - - bool didFallback = false; - const auto face = _ResolveFontFaceWithFallback(fontName, weight, stretch, style, fontLocaleName, didFallback); - - 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; - bool didItalicFallback = false; - - const auto faceItalic = _ResolveFontFaceWithFallback(fontNameItalic, weightItalic, stretchItalic, styleItalic, fontLocaleName, didItalicFallback); - - 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); - actual.SetFallback(didFallback); - - 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; + // Initialize the default font info and build everything from here. + _defaultFontInfo = DxFontInfo(desired.GetFaceName(), + desired.GetWeight(), + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL); - _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)); + _BuildFontRenderData(desired, actual, dpi); } CATCH_RETURN(); @@ -622,288 +442,212 @@ 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. -// 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. -// - localeName - Locale to search for appropriate fonts -// - didFallback - Indicates whether we couldn't match the user request and had to choose from a hardcoded default list. -// 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, - bool& didFallback) const -{ - // First attempt to find exactly what the user asked for. - didFallback = false; - Microsoft::WRL::ComPtr face{ nullptr }; - - // 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 - { - face = _FindFontFace(familyName, weight, stretch, style, localeName, true); - - if (!face) - { - // 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); - - // Try to find it with the shortened family name - face = _FindFontFace(familyName, weight, stretch, style, 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(familyName, weight, stretch, style, 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(familyName, weight, stretch, style, localeName, false); - - if (face) - { - didFallback = true; - break; - } - } - } - - THROW_HR_IF_NULL(E_FAIL, face); - - return face; -} - -// Routine Description: -// - Locates a suitable font face from the given information +// - 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::_FindFontFace(std::wstring& familyName, - DWRITE_FONT_WEIGHT& weight, - DWRITE_FONT_STRETCH& stretch, - DWRITE_FONT_STYLE& style, - std::wstring& localeName, - const bool withNearbyLookup) const +// - None +void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi) { - Microsoft::WRL::ComPtr fontFace; - - Microsoft::WRL::ComPtr fontCollection; - THROW_IF_FAILED(_dwriteFactory->GetSystemFontCollection(&fontCollection, false)); - - UINT32 familyIndex; - BOOL familyExists; - THROW_IF_FAILED(fontCollection->FindFamilyName(familyName.data(), &familyIndex, &familyExists)); - - // If the system collection missed, try the files sitting next to our binary. - if (withNearbyLookup && !familyExists) - { - auto&& nearbyCollection = NearbyCollection(); - - // May be null on OS below Windows 10. If null, just skip the attempt. - if (nearbyCollection) - { - nearbyCollection.As(&fontCollection); - THROW_IF_FAILED(fontCollection->FindFamilyName(familyName.data(), &familyIndex, &familyExists)); - } - } - - if (familyExists) - { - Microsoft::WRL::ComPtr fontFamily; - THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily)); + std::wstring fontLocaleName = UserLocaleName(); + // 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); - Microsoft::WRL::ComPtr font; - THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(weight, stretch, style, &font)); - - Microsoft::WRL::ComPtr fontFace0; - THROW_IF_FAILED(font->CreateFontFace(&fontFace0)); - - THROW_IF_FAILED(fontFace0.As(&fontFace)); - - // Retrieve metrics in case the font we created was different than what was requested. - weight = font->GetWeight(); - stretch = font->GetStretch(); - style = font->GetStyle(); - - // Dig the family name out at the end to return it. - familyName = _GetFontFamilyName(fontFamily.Get(), localeName); - } - - return fontFace; -} - -// Routine Description: -// - Retrieves the font family name out of the given object in the given locale. -// - If we can't find a valid name for the given locale, we'll fallback and report it back. -// Arguments: -// - fontFamily - DirectWrite font family object -// - localeName - The locale in which the name should be retrieved. -// - If fallback occurred, this is updated to what we retrieved instead. -// Return Value: -// - Localized string name of the font family -[[nodiscard]] std::wstring DxFontRenderData::_GetFontFamilyName(gsl::not_null const fontFamily, - std::wstring& localeName) const -{ - // See: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontcollection - Microsoft::WRL::ComPtr familyNames; - THROW_IF_FAILED(fontFamily->GetFamilyNames(&familyNames)); - - // First we have to find the right family name for the locale. We're going to bias toward what the caller - // requested, but fallback if we need to and reply with the locale we ended up choosing. - UINT32 index = 0; - BOOL exists = false; - - // This returns S_OK whether or not it finds a locale name. Check exists field instead. - // If it returns an error, it's a real problem, not an absence of this locale name. - // https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-findlocalename - THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists)); - - // If we tried and it still doesn't exist, try with the fallback locale. - if (!exists) - { - localeName = FALLBACK_LOCALE; - THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists)); - } + DWRITE_FONT_METRICS1 fontMetrics; + face->GetMetrics(&fontMetrics); - // If it still doesn't exist, we're going to try index 0. - if (!exists) + 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); + + actual.SetFallback(_defaultFontInfo.GetFallback()); + + 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) { - 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)); + lineMetrics.underlineOffset2 = lineMetrics.underlineOffset - lineMetrics.gridlineWidth; } - // 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)); + // 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; - // and return it. - return retVal; -} - -[[nodiscard]] std::wstring DxFontRenderData::_GetUserLocaleName() -{ - if (_userLocaleName.empty()) - { - std::array localeName; - - const auto returnCode = GetUserDefaultLocaleName(localeName.data(), gsl::narrow(localeName.size())); - if (returnCode) - { - _userLocaleName = { localeName.data() }; - } - else - { - _userLocaleName = { FALLBACK_LOCALE.data(), FALLBACK_LOCALE.size() }; - } - } + _lineMetrics = lineMetrics; - return _userLocaleName; + _glyphCell = actual.GetSize(); } -// 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() +Microsoft::WRL::ComPtr DxFontRenderData::_BuildTextFormat(const DxFontInfo fontInfo, const std::wstring_view localeName) { - 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; + 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 b0ed213914e..990bcbd966e 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,73 +32,69 @@ 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(); - [[nodiscard]] const Microsoft::WRL::ComPtr& NearbyCollection() const; + // 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 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() 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; + // 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() noexcept; + // 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; [[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, - bool& didFallback) 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 bool withNearbyLookup) 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(); - - [[nodiscard]] static std::vector s_GetNearbyFonts(); + 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; ::Microsoft::WRL::ComPtr _dwriteTextAnalyzer; - ::Microsoft::WRL::ComPtr _dwriteTextFormat; - ::Microsoft::WRL::ComPtr _dwriteTextFormatItalic; - ::Microsoft::WRL::ComPtr _dwriteFontFace; - ::Microsoft::WRL::ComPtr _dwriteFontFaceItalic; + + std::unordered_map> _textFormatMap; + std::unordered_map> _fontFaceMap; ::Microsoft::WRL::ComPtr _boxDrawingEffect; ::Microsoft::WRL::ComPtr _systemFontFallback; mutable ::Microsoft::WRL::ComPtr _nearbyCollection; 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 \