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.
This commit is contained in:
parent
1b79cc87c3
commit
85c485e94f
|
@ -133,10 +133,21 @@ CATCH_RETURN()
|
|||
_In_ IDWriteTextRenderer* renderer,
|
||||
FLOAT originX,
|
||||
FLOAT originY) noexcept
|
||||
try
|
||||
{
|
||||
const auto drawingContext = static_cast<const DrawingContext*>(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
|
||||
|
|
405
src/renderer/dx/DxFontInfo.cpp
Normal file
405
src/renderer/dx/DxFontInfo.cpp
Normal file
|
@ -0,0 +1,405 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "precomp.h"
|
||||
|
||||
#include "DxFontInfo.h"
|
||||
|
||||
#include "unicode.hpp"
|
||||
|
||||
#include <VersionHelpers.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),
|
||||
_didFallback(false)
|
||||
{
|
||||
}
|
||||
|
||||
DxFontInfo::DxFontInfo(std::wstring_view familyName,
|
||||
unsigned int weight,
|
||||
DWRITE_FONT_STYLE style,
|
||||
DWRITE_FONT_STRETCH stretch) :
|
||||
DxFontInfo(familyName, static_cast<DWRITE_FONT_WEIGHT>(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<IDWriteFontFace1> DxFontInfo::ResolveFontFaceWithFallback(gsl::not_null<IDWriteFactory1*> dwriteFactory,
|
||||
std::wstring& localeName)
|
||||
{
|
||||
// First attempt to find exactly what the user asked for.
|
||||
_didFallback = false;
|
||||
Microsoft::WRL::ComPtr<IDWriteFontFace1> 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<IDWriteFontFace1> DxFontInfo::_FindFontFace(gsl::not_null<IDWriteFactory1*> dwriteFactory, std::wstring& localeName, const bool withNearbyLookup)
|
||||
{
|
||||
Microsoft::WRL::ComPtr<IDWriteFontFace1> fontFace;
|
||||
|
||||
Microsoft::WRL::ComPtr<IDWriteFontCollection> 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<IDWriteFontFamily> fontFamily;
|
||||
THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily));
|
||||
|
||||
Microsoft::WRL::ComPtr<IDWriteFont> font;
|
||||
THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(GetWeight(), GetStretch(), GetStyle(), &font));
|
||||
|
||||
Microsoft::WRL::ComPtr<IDWriteFontFace> 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<IDWriteFontFamily*> const fontFamily,
|
||||
std::wstring& localeName)
|
||||
{
|
||||
// See: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontcollection
|
||||
Microsoft::WRL::ComPtr<IDWriteLocalizedStrings> 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<IDWriteFontCollection1>& DxFontInfo::_NearbyCollection(gsl::not_null<IDWriteFactory1*> 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<IDWriteFactory3> factory3;
|
||||
THROW_IF_FAILED(dwriteFactory->QueryInterface<IDWriteFactory3>(&factory3));
|
||||
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontSetBuilder> fontSetBuilder;
|
||||
THROW_IF_FAILED(factory3->CreateFontSetBuilder(&fontSetBuilder));
|
||||
|
||||
// Builder2 has a convenience to just feed in paths to font files.
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontSetBuilder2> fontSetBuilder2;
|
||||
THROW_IF_FAILED(fontSetBuilder.As(&fontSetBuilder2));
|
||||
|
||||
for (auto& p : knownPaths)
|
||||
{
|
||||
fontSetBuilder2->AddFontFile(p.c_str());
|
||||
}
|
||||
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontSet> 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:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - Iterable collection of filesystem paths, one per font file that was found
|
||||
[[nodiscard]] std::vector<std::filesystem::path> DxFontInfo::s_GetNearbyFonts()
|
||||
{
|
||||
std::vector<std::filesystem::path> 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<std::wstring>(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;
|
||||
}
|
108
src/renderer/dx/DxFontInfo.h
Normal file
108
src/renderer/dx/DxFontInfo.h
Normal file
|
@ -0,0 +1,108 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <dwrite.h>
|
||||
#include <dwrite_1.h>
|
||||
#include <dwrite_2.h>
|
||||
#include <dwrite_3.h>
|
||||
|
||||
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<IDWriteFontFace1> ResolveFontFaceWithFallback(gsl::not_null<IDWriteFactory1*> dwriteFactory,
|
||||
std::wstring& localeName);
|
||||
|
||||
private:
|
||||
[[nodiscard]] ::Microsoft::WRL::ComPtr<IDWriteFontFace1> _FindFontFace(gsl::not_null<IDWriteFactory1*> dwriteFactory,
|
||||
std::wstring& localeName,
|
||||
const bool withNearbyLookup);
|
||||
|
||||
[[nodiscard]] std::wstring _GetFontFamilyName(gsl::not_null<IDWriteFontFamily*> const fontFamily,
|
||||
std::wstring& localeName);
|
||||
|
||||
[[nodiscard]] const Microsoft::WRL::ComPtr<IDWriteFontCollection1>& _NearbyCollection(gsl::not_null<IDWriteFactory1*> dwriteFactory) const;
|
||||
|
||||
[[nodiscard]] static std::vector<std::filesystem::path> s_GetNearbyFonts();
|
||||
|
||||
mutable ::Microsoft::WRL::ComPtr<IDWriteFontCollection1> _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<Microsoft::Console::Render::DxFontInfo>
|
||||
{
|
||||
size_t operator()(const Microsoft::Console::Render::DxFontInfo& fontInfo) const noexcept
|
||||
{
|
||||
const size_t h1 = std::hash<std::wstring_view>{}(fontInfo.GetFamilyName());
|
||||
const size_t h2 = std::hash<DWRITE_FONT_WEIGHT>{}(fontInfo.GetWeight());
|
||||
const size_t h3 = std::hash<DWRITE_FONT_STYLE>{}(fontInfo.GetStyle());
|
||||
const size_t h4 = std::hash<DWRITE_FONT_STRETCH>{}(fontInfo.GetStretch());
|
||||
const size_t h5 = std::hash<bool>{}(fontInfo.GetFallback());
|
||||
|
||||
static const auto combine = [](std::initializer_list<size_t> 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 });
|
||||
}
|
||||
};
|
||||
}
|
|
@ -17,14 +17,22 @@ using namespace Microsoft::Console::Render;
|
|||
|
||||
DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr<IDWriteFactory1> dwriteFactory) noexcept :
|
||||
_dwriteFactory(dwriteFactory),
|
||||
_fontSize{},
|
||||
_glyphCell{},
|
||||
_lineMetrics({}),
|
||||
_boxDrawingEffect{}
|
||||
_lineMetrics{},
|
||||
_lineSpacing{}
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> DxFontRenderData::Analyzer() noexcept
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> DxFontRenderData::Analyzer()
|
||||
{
|
||||
if (!_dwriteTextAnalyzer)
|
||||
{
|
||||
Microsoft::WRL::ComPtr<IDWriteTextAnalyzer> analyzer;
|
||||
THROW_IF_FAILED(_dwriteFactory->CreateTextAnalyzer(&analyzer));
|
||||
THROW_IF_FAILED(analyzer.As(&_dwriteTextAnalyzer));
|
||||
}
|
||||
|
||||
return _dwriteTextAnalyzer;
|
||||
}
|
||||
|
||||
|
@ -40,49 +48,24 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr<IDWriteFactory1> 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:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - DirectWrite font collection. May be null if one cannot be created.
|
||||
[[nodiscard]] const Microsoft::WRL::ComPtr<IDWriteFontCollection1>& 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<IDWriteFactory3> factory3;
|
||||
THROW_IF_FAILED(_dwriteFactory.As(&factory3));
|
||||
std::array<wchar_t, LOCALE_NAME_MAX_LENGTH> localeName;
|
||||
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontSetBuilder> fontSetBuilder;
|
||||
THROW_IF_FAILED(factory3->CreateFontSetBuilder(&fontSetBuilder));
|
||||
|
||||
// Builder2 has a convenience to just feed in paths to font files.
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontSetBuilder2> fontSetBuilder2;
|
||||
THROW_IF_FAILED(fontSetBuilder.As(&fontSetBuilder2));
|
||||
|
||||
for (auto& p : knownPaths)
|
||||
const auto returnCode = GetUserDefaultLocaleName(localeName.data(), gsl::narrow<int>(localeName.size()));
|
||||
if (returnCode)
|
||||
{
|
||||
fontSetBuilder2->AddFontFile(p.c_str());
|
||||
_userLocaleName = { localeName.data() };
|
||||
}
|
||||
else
|
||||
{
|
||||
_userLocaleName = { FALLBACK_LOCALE.data(), FALLBACK_LOCALE.size() };
|
||||
}
|
||||
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontSet> 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<IDWriteFactory1> dwr
|
|||
return _lineMetrics;
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> DxFontRenderData::DefaultTextFormat() noexcept
|
||||
[[nodiscard]] DWRITE_FONT_WEIGHT DxFontRenderData::DefaultFontWeight() noexcept
|
||||
{
|
||||
return _dwriteTextFormat;
|
||||
return _defaultFontInfo.GetWeight();
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontRenderData::DefaultFontFace() noexcept
|
||||
[[nodiscard]] DWRITE_FONT_STYLE DxFontRenderData::DefaultFontStyle() noexcept
|
||||
{
|
||||
return _dwriteFontFace;
|
||||
return _defaultFontInfo.GetStyle();
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IBoxDrawingEffect> DxFontRenderData::DefaultBoxDrawingEffect() noexcept
|
||||
[[nodiscard]] DWRITE_FONT_STRETCH DxFontRenderData::DefaultFontStretch() noexcept
|
||||
{
|
||||
return _defaultFontInfo.GetStretch();
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> DxFontRenderData::DefaultTextFormat()
|
||||
{
|
||||
return TextFormatWithAttribute(_defaultFontInfo.GetWeight(), _defaultFontInfo.GetStyle(), _defaultFontInfo.GetStretch());
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DxFontRenderData::DefaultFontFace()
|
||||
{
|
||||
return FontFaceWithAttribute(_defaultFontInfo.GetWeight(), _defaultFontInfo.GetStyle(), _defaultFontInfo.GetStretch());
|
||||
}
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IBoxDrawingEffect> 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<IDWriteTextFormat> DxFontRenderData::ItalicTextFormat() noexcept
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> 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<IDWriteTextFormat> 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<IDWriteFontFace1> DxFontRenderData::ItalicFontFace() noexcept
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> 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<IDWriteFontFace1> 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<IDWriteFactory1> dwr
|
|||
try
|
||||
{
|
||||
_userLocaleName.clear();
|
||||
_textFormatMap.clear();
|
||||
_fontFaceMap.clear();
|
||||
_boxDrawingEffect.Reset();
|
||||
|
||||
std::wstring fontName(desired.GetFaceName());
|
||||
DWRITE_FONT_WEIGHT weight = static_cast<DWRITE_FONT_WEIGHT>(desired.GetWeight());
|
||||
DWRITE_FONT_STYLE style = DWRITE_FONT_STYLE_NORMAL;
|
||||
DWRITE_FONT_STRETCH stretch = DWRITE_FONT_STRETCH_NORMAL;
|
||||
std::wstring localeName = _GetUserLocaleName();
|
||||
// Initialize the default font info and build everything from here.
|
||||
_defaultFontInfo = DxFontInfo(desired.GetFaceName(),
|
||||
desired.GetWeight(),
|
||||
DWRITE_FONT_STYLE_NORMAL,
|
||||
DWRITE_FONT_STRETCH_NORMAL);
|
||||
|
||||
// _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<float>(desired.GetEngineSize().Y) * static_cast<float>(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<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI));
|
||||
|
||||
const float widthAdvance = static_cast<float>(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<IDWriteTextFormat> 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<IDWriteTextFormat> 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<IDWriteTextAnalyzer> 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<SHORT>(widthExact);
|
||||
coordSize.Y = gsl::narrow_cast<SHORT>(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;
|
||||
|
||||
_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.
|
||||
// - 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.
|
||||
// - 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.
|
||||
// - 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<IDWriteFontFace1> DxFontRenderData::_ResolveFontFaceWithFallback(std::wstring& familyName,
|
||||
DWRITE_FONT_WEIGHT& weight,
|
||||
DWRITE_FONT_STRETCH& stretch,
|
||||
DWRITE_FONT_STYLE& style,
|
||||
std::wstring& localeName,
|
||||
bool& didFallback) const
|
||||
// - None
|
||||
void DxFontRenderData::_BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi)
|
||||
{
|
||||
// First attempt to find exactly what the user asked for.
|
||||
didFallback = false;
|
||||
Microsoft::WRL::ComPtr<IDWriteFontFace1> face{ nullptr };
|
||||
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<IDWriteFontFace1> face = _defaultFontInfo.ResolveFontFaceWithFallback(_dwriteFactory.Get(), fontLocaleName);
|
||||
|
||||
// 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
|
||||
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<float>(desired.GetEngineSize().Y) * static_cast<float>(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<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI));
|
||||
|
||||
const float widthAdvance = static_cast<float>(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<SHORT>(widthExact);
|
||||
coordSize.Y = gsl::narrow_cast<SHORT>(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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
lineMetrics.underlineOffset2 = lineMetrics.underlineOffset - lineMetrics.gridlineWidth;
|
||||
}
|
||||
|
||||
THROW_HR_IF_NULL(E_FAIL, face);
|
||||
// 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;
|
||||
|
||||
return face;
|
||||
_lineMetrics = lineMetrics;
|
||||
|
||||
_glyphCell = actual.GetSize();
|
||||
}
|
||||
|
||||
// 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<IDWriteFontFace1> DxFontRenderData::_FindFontFace(std::wstring& familyName,
|
||||
DWRITE_FONT_WEIGHT& weight,
|
||||
DWRITE_FONT_STRETCH& stretch,
|
||||
DWRITE_FONT_STYLE& style,
|
||||
std::wstring& localeName,
|
||||
const bool withNearbyLookup) const
|
||||
Microsoft::WRL::ComPtr<IDWriteTextFormat> DxFontRenderData::_BuildTextFormat(const DxFontInfo fontInfo, const std::wstring_view localeName)
|
||||
{
|
||||
Microsoft::WRL::ComPtr<IDWriteFontFace1> fontFace;
|
||||
|
||||
Microsoft::WRL::ComPtr<IDWriteFontCollection> 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<IDWriteFontFamily> fontFamily;
|
||||
THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily));
|
||||
|
||||
Microsoft::WRL::ComPtr<IDWriteFont> font;
|
||||
THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(weight, stretch, style, &font));
|
||||
|
||||
Microsoft::WRL::ComPtr<IDWriteFontFace> 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<IDWriteFontFamily*> const fontFamily,
|
||||
std::wstring& localeName) const
|
||||
{
|
||||
// See: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontcollection
|
||||
Microsoft::WRL::ComPtr<IDWriteLocalizedStrings> familyNames;
|
||||
THROW_IF_FAILED(fontFamily->GetFamilyNames(&familyNames));
|
||||
|
||||
// First we have to find the right family name for the locale. We're going to bias toward what the caller
|
||||
// requested, but fallback if we need to and reply with the locale we ended up choosing.
|
||||
UINT32 index = 0;
|
||||
BOOL exists = false;
|
||||
|
||||
// This returns S_OK whether or not it finds a locale name. Check exists field instead.
|
||||
// If it returns an error, it's a real problem, not an absence of this locale name.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-findlocalename
|
||||
THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists));
|
||||
|
||||
// If we tried and it still doesn't exist, try with the fallback locale.
|
||||
if (!exists)
|
||||
{
|
||||
localeName = FALLBACK_LOCALE;
|
||||
THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists));
|
||||
}
|
||||
|
||||
// If it still doesn't exist, we're going to try index 0.
|
||||
if (!exists)
|
||||
{
|
||||
index = 0;
|
||||
|
||||
// Get the locale name out so at least the caller knows what locale this name goes with.
|
||||
UINT32 length = 0;
|
||||
THROW_IF_FAILED(familyNames->GetLocaleNameLength(index, &length));
|
||||
localeName.resize(length);
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getlocalenamelength
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getlocalename
|
||||
// GetLocaleNameLength does not include space for null terminator, but GetLocaleName needs it so add one.
|
||||
THROW_IF_FAILED(familyNames->GetLocaleName(index, localeName.data(), length + 1));
|
||||
}
|
||||
|
||||
// OK, now that we've decided which family name and the locale that it's in... let's go get it.
|
||||
UINT32 length = 0;
|
||||
THROW_IF_FAILED(familyNames->GetStringLength(index, &length));
|
||||
|
||||
// Make our output buffer and resize it so it is allocated.
|
||||
std::wstring retVal;
|
||||
retVal.resize(length);
|
||||
|
||||
// FINALLY, go fetch the string name.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getstringlength
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-getstring
|
||||
// Once again, GetStringLength is without the null, but GetString needs the null. So add one.
|
||||
THROW_IF_FAILED(familyNames->GetString(index, retVal.data(), length + 1));
|
||||
|
||||
// and return it.
|
||||
return retVal;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::wstring DxFontRenderData::_GetUserLocaleName()
|
||||
{
|
||||
if (_userLocaleName.empty())
|
||||
{
|
||||
std::array<wchar_t, LOCALE_NAME_MAX_LENGTH> localeName;
|
||||
|
||||
const auto returnCode = GetUserDefaultLocaleName(localeName.data(), gsl::narrow<int>(localeName.size()));
|
||||
if (returnCode)
|
||||
{
|
||||
_userLocaleName = { localeName.data() };
|
||||
}
|
||||
else
|
||||
{
|
||||
_userLocaleName = { FALLBACK_LOCALE.data(), FALLBACK_LOCALE.size() };
|
||||
}
|
||||
}
|
||||
|
||||
return _userLocaleName;
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
// - Digs through the directory that the current executable is running within to find
|
||||
// any TTF files sitting next to it.
|
||||
// Arguments:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - Iterable collection of filesystem paths, one per font file that was found
|
||||
[[nodiscard]] std::vector<std::filesystem::path> DxFontRenderData::s_GetNearbyFonts()
|
||||
{
|
||||
std::vector<std::filesystem::path> 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<std::wstring>(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<IDWriteTextFormat> format;
|
||||
THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontInfo.GetFamilyName().data(),
|
||||
nullptr,
|
||||
fontInfo.GetWeight(),
|
||||
fontInfo.GetStyle(),
|
||||
fontInfo.GetStretch(),
|
||||
_fontSize,
|
||||
localeName.data(),
|
||||
&format));
|
||||
return format;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "../../renderer/inc/FontInfoDesired.hpp"
|
||||
#include "DxFontInfo.h"
|
||||
#include "BoxDrawingEffect.h"
|
||||
|
||||
#include <dwrite.h>
|
||||
|
@ -31,73 +32,69 @@ namespace Microsoft::Console::Render
|
|||
DxFontRenderData(::Microsoft::WRL::ComPtr<IDWriteFactory1> dwriteFactory) noexcept;
|
||||
|
||||
// DirectWrite text analyzer from the factory
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> Analyzer() noexcept;
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> Analyzer();
|
||||
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFallback> SystemFontFallback();
|
||||
|
||||
[[nodiscard]] const Microsoft::WRL::ComPtr<IDWriteFontCollection1>& 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<IDWriteTextFormat> DefaultTextFormat() noexcept;
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> DefaultTextFormat();
|
||||
|
||||
// The DirectWrite font face to use while calculating layout (by default)
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DefaultFontFace() noexcept;
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> DefaultFontFace();
|
||||
|
||||
// Box drawing scaling effects that are cached for the base font across layouts
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IBoxDrawingEffect> DefaultBoxDrawingEffect() noexcept;
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IBoxDrawingEffect> DefaultBoxDrawingEffect();
|
||||
|
||||
// The italic variant of the format object representing the size and other text properties for italic text
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> ItalicTextFormat() noexcept;
|
||||
// The attributed variants of the format object representing the size and other text properties
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteTextFormat> 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<IDWriteFontFace1> ItalicFontFace() noexcept;
|
||||
// The attributed variants of the font face to use while calculating layout
|
||||
[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFace1> 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<IDWriteFontFace1> _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<IDWriteFontFace1> _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<IDWriteFontFamily*> 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<std::filesystem::path> s_GetNearbyFonts();
|
||||
void _BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi);
|
||||
Microsoft::WRL::ComPtr<IDWriteTextFormat> _BuildTextFormat(const DxFontInfo fontInfo, const std::wstring_view localeName);
|
||||
|
||||
::Microsoft::WRL::ComPtr<IDWriteFactory1> _dwriteFactory;
|
||||
|
||||
::Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> _dwriteTextAnalyzer;
|
||||
::Microsoft::WRL::ComPtr<IDWriteTextFormat> _dwriteTextFormat;
|
||||
::Microsoft::WRL::ComPtr<IDWriteTextFormat> _dwriteTextFormatItalic;
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontFace1> _dwriteFontFace;
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontFace1> _dwriteFontFaceItalic;
|
||||
|
||||
std::unordered_map<DxFontInfo, ::Microsoft::WRL::ComPtr<IDWriteTextFormat>> _textFormatMap;
|
||||
std::unordered_map<DxFontInfo, ::Microsoft::WRL::ComPtr<IDWriteFontFace1>> _fontFaceMap;
|
||||
|
||||
::Microsoft::WRL::ComPtr<IBoxDrawingEffect> _boxDrawingEffect;
|
||||
|
||||
::Microsoft::WRL::ComPtr<IDWriteFontFallback> _systemFontFallback;
|
||||
mutable ::Microsoft::WRL::ComPtr<IDWriteFontCollection1> _nearbyCollection;
|
||||
std::wstring _userLocaleName;
|
||||
DxFontInfo _defaultFontInfo;
|
||||
|
||||
float _fontSize;
|
||||
til::size _glyphCell;
|
||||
|
||||
DWRITE_LINE_SPACING _lineSpacing;
|
||||
LineMetrics _lineMetrics;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
<ClCompile Include="..\precomp.cpp">
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\DxFontInfo.cpp" />
|
||||
<ClCompile Include="..\DxFontRenderData.cpp" />
|
||||
<ClCompile Include="..\DxRenderer.cpp" />
|
||||
</ItemGroup>
|
||||
|
@ -30,6 +31,7 @@
|
|||
<ClInclude Include="..\CustomTextRenderer.h" />
|
||||
<ClInclude Include="..\precomp.h" />
|
||||
<ClInclude Include="..\DxRenderer.hpp" />
|
||||
<ClInclude Include="..\DxFontInfo.h" />
|
||||
<ClInclude Include="..\DxFontRenderData.h" />
|
||||
<ClInclude Include="..\ScreenPixelShader.h" />
|
||||
<ClInclude Include="..\ScreenVertexShader.h" />
|
||||
|
|
|
@ -33,6 +33,7 @@ INCLUDES = \
|
|||
SOURCES = \
|
||||
$(SOURCES) \
|
||||
..\DxRenderer.cpp \
|
||||
..\DxFontInfo.cpp \
|
||||
..\DxFontRenderData.cpp \
|
||||
..\CustomTextRenderer.cpp \
|
||||
..\CustomTextLayout.cpp \
|
||||
|
|
Loading…
Reference in a new issue