Skip to content

Commit

Permalink
Reduce instances of font fallback dialog (#9734)
Browse files Browse the repository at this point in the history
Reduce instances of font fallback dialog through package font loading,
basic name trimming, and revised fallback test

- Adjusts the font dialog to only show when we attempt last-chance
  resolution from our hardcoded list of font names with a flag instead
  of with a string comparison by name
- Adds a resolution step to trim the font name by word from the end and
  retry to attempt to resolve a proper font that just has a weight
  suffix
- Adds a second font collection to font loading that will attempt to
  locate all TTF files sitting next to our binary, like in our package

- [x] Wrote my font preference in the JSON as `Cascadia Code Heavy` and
  watched it quietly resolve to just `Cascadia Code` without the dialog.
- [x] Put a font that isn't registered with the system into the layout
  directory for the package, set it as my desired font in Terminal, and
  watched it load just fine.
- [x] Try a font name with different casing and see if dialog doesn't
  pop anymore
- [x] Try a font with different (localized) names like MS ゴシック and
  see if dialog doesn't pop anymore
- [x] Check Win7 with WPF target

Closes #9375
  • Loading branch information
miniksa authored Apr 8, 2021
1 parent ed1cd32 commit 7f5a19b
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 17 deletions.
4 changes: 2 additions & 2 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2116,8 +2116,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// actually fail. We need a way to gracefully fallback.
_renderer->TriggerFontChange(newDpi, _desiredFont, _actualFont);

// If the actual font isn't what was requested...
if (_actualFont.GetFaceName() != _desiredFont.GetFaceName())
// If the actual font went through the last-chance fallback routines...
if (_actualFont.GetFallback())
{
// Then warn the user that we picked something because we couldn't find their font.

Expand Down
13 changes: 12 additions & 1 deletion src/renderer/base/fontinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ FontInfo::FontInfo(const std::wstring_view faceName,
const bool fSetDefaultRasterFont /* = false */) :
FontInfoBase(faceName, family, weight, fSetDefaultRasterFont, codePage),
_coordSize(coordSize),
_coordSizeUnscaled(coordSize)
_coordSizeUnscaled(coordSize),
_didFallback(false)
{
ValidateFont();
}
Expand Down Expand Up @@ -60,6 +61,16 @@ void FontInfo::SetFromEngine(const std::wstring_view faceName,
_ValidateCoordSize();
}

bool FontInfo::GetFallback() const noexcept
{
return _didFallback;
}

void FontInfo::SetFallback(const bool didFallback) noexcept
{
_didFallback = didFallback;
}

void FontInfo::ValidateFont()
{
_ValidateCoordSize();
Expand Down
165 changes: 152 additions & 13 deletions src/renderer/dx/DxFontRenderData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

#include "DxFontRenderData.h"

#include "unicode.hpp"

#include <VersionHelpers.h>

static constexpr float POINTS_PER_INCH = 72.0f;
static constexpr std::wstring_view FALLBACK_FONT_FACES[] = { L"Consolas", L"Lucida Console", L"Courier New" };
static constexpr std::wstring_view FALLBACK_LOCALE = L"en-us";
Expand Down Expand Up @@ -36,6 +40,51 @@ 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
{
// 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.As(&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;
}

[[nodiscard]] til::size DxFontRenderData::GlyphCell() noexcept
{
return _glyphCell;
Expand Down Expand Up @@ -94,7 +143,9 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr<IDWriteFactory1> dwr
// _ResolveFontFaceWithFallback overrides the last argument with the locale name of the font,
// but we should use the system's locale to render the text.
std::wstring fontLocaleName = localeName;
const auto face = _ResolveFontFaceWithFallback(fontName, weight, stretch, style, fontLocaleName);

bool didFallback = false;
const auto face = _ResolveFontFaceWithFallback(fontName, weight, stretch, style, fontLocaleName, didFallback);

DWRITE_FONT_METRICS1 fontMetrics;
face->GetMetrics(&fontMetrics);
Expand Down Expand Up @@ -221,8 +272,9 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr<IDWriteFactory1> dwr
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);
const auto faceItalic = _ResolveFontFaceWithFallback(fontNameItalic, weightItalic, stretchItalic, styleItalic, fontLocaleName, didItalicFallback);

Microsoft::WRL::ComPtr<IDWriteTextFormat> formatItalic;
THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontNameItalic.data(),
Expand Down Expand Up @@ -266,6 +318,7 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr<IDWriteFactory1> dwr
false,
scaled,
unscaled);
actual.SetFallback(didFallback);

LineMetrics lineMetrics;
// There is no font metric for the grid line width, so we use a small
Expand Down Expand Up @@ -578,37 +631,76 @@ CATCH_RETURN()
// - 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<IDWriteFontFace1> DxFontRenderData::_ResolveFontFaceWithFallback(std::wstring& familyName,
DWRITE_FONT_WEIGHT& weight,
DWRITE_FONT_STRETCH& stretch,
DWRITE_FONT_STYLE& style,
std::wstring& localeName) const
std::wstring& localeName,
bool& didFallback) const
{
// First attempt to find exactly what the user asked for.
didFallback = false;
auto face = _FindFontFace(familyName, weight, stretch, style, localeName);

if (!face)
{
for (const auto fallbackFace : FALLBACK_FONT_FACES)
// If we missed, try looking a little more by trimming the last word off the requested family name a few times.
// Quite often, folks are specifying weights or something in the familyName and it causes failed resolution and
// an unexpected error dialog. We theoretically could detect the weight words and convert them, but this
// is the quick fix for the majority scenario.
// The long/full fix is backlogged to GH#9744
// Also this doesn't count as a fallback because we don't want to annoy folks with the warning dialog over
// this resolution.
while (!face && !familyName.empty())
{
familyName = fallbackFace;
face = _FindFontFace(familyName, weight, stretch, style, localeName);
const auto lastSpace = familyName.find_last_of(UNICODE_SPACE);

if (face)
// value is unsigned and npos will be greater than size.
// if we didn't find anything to trim, leave.
if (lastSpace >= familyName.size())
{
break;
}

familyName = fallbackFace;
weight = DWRITE_FONT_WEIGHT_NORMAL;
stretch = DWRITE_FONT_STRETCH_NORMAL;
style = DWRITE_FONT_STYLE_NORMAL;
// 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);
}

if (face)
// 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)
{
break;
familyName = fallbackFace;
face = _FindFontFace(familyName, weight, stretch, style, localeName);

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);

if (face)
{
didFallback = true;
break;
}
}
}
}
Expand Down Expand Up @@ -642,6 +734,19 @@ CATCH_RETURN()
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 (!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;
Expand Down Expand Up @@ -753,3 +858,37 @@ CATCH_RETURN()

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;
}
8 changes: 7 additions & 1 deletion src/renderer/dx/DxFontRenderData.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ namespace Microsoft::Console::Render

[[nodiscard]] Microsoft::WRL::ComPtr<IDWriteFontFallback> SystemFontFallback();

[[nodiscard]] const Microsoft::WRL::ComPtr<IDWriteFontCollection1>& NearbyCollection() const;

[[nodiscard]] til::size GlyphCell() noexcept;
[[nodiscard]] LineMetrics GetLineMetrics() noexcept;

Expand Down Expand Up @@ -62,7 +64,8 @@ namespace Microsoft::Console::Render
DWRITE_FONT_WEIGHT& weight,
DWRITE_FONT_STRETCH& stretch,
DWRITE_FONT_STYLE& style,
std::wstring& localeName) const;
std::wstring& localeName,
bool& didFallback) const;

[[nodiscard]] ::Microsoft::WRL::ComPtr<IDWriteFontFace1> _FindFontFace(std::wstring& familyName,
DWRITE_FONT_WEIGHT& weight,
Expand All @@ -76,6 +79,8 @@ namespace Microsoft::Console::Render
// 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();

::Microsoft::WRL::ComPtr<IDWriteFactory1> _dwriteFactory;

::Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> _dwriteTextAnalyzer;
Expand All @@ -87,6 +92,7 @@ namespace Microsoft::Console::Render
::Microsoft::WRL::ComPtr<IBoxDrawingEffect> _boxDrawingEffect;

::Microsoft::WRL::ComPtr<IDWriteFontFallback> _systemFontFallback;
mutable ::Microsoft::WRL::ComPtr<IDWriteFontCollection1> _nearbyCollection;
std::wstring _userLocaleName;

til::size _glyphCell;
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/inc/FontInfo.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class FontInfo : public FontInfoBase
const COORD coordSize,
const COORD coordSizeUnscaled);

bool GetFallback() const noexcept;
void SetFallback(const bool didFallback) noexcept;

void ValidateFont();

friend bool operator==(const FontInfo& a, const FontInfo& b);
Expand All @@ -56,6 +59,7 @@ class FontInfo : public FontInfoBase

COORD _coordSize;
COORD _coordSizeUnscaled;
bool _didFallback;
};

bool operator==(const FontInfo& a, const FontInfo& b);
Expand Down

0 comments on commit 7f5a19b

Please sign in to comment.