Skip to content

Commit

Permalink
Add support for customizing font fallback (#16821)
Browse files Browse the repository at this point in the history
This adds support for specifying more than one font family using a
syntax that is similar to CSS' `font-family` property.
The implementation is straight-forward and is effectively
just a wrapper around `IDWriteFontFallbackBuilder`.

Closes #2664

## PR Checklist
* Font fallback
  * Write "「猫」"
  * Use "Consolas" and remember the shape of the glyphs
  * Use "Consolas, MS Gothic" and check that it changed ✅
* Settings UI autocompletion
  * It completes ✅
  * It filters ✅
  * It recognizes commas and starts a new name ✅
* All invalid font names are listed in the warning message ✅

---------

Co-authored-by: Dustin L. Howett <duhowett@microsoft.com>
  • Loading branch information
lhecker and DHowett committed Mar 26, 2024
1 parent a67a132 commit de7f931
Show file tree
Hide file tree
Showing 28 changed files with 727 additions and 312 deletions.
1 change: 1 addition & 0 deletions .github/actions/spelling/expect/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,7 @@ NOCONTEXTHELP
NOCOPYBITS
NODUP
noexcepts
NOFONT
NOINTEGRALHEIGHT
NOINTERFACE
NOLINKINFO
Expand Down
27 changes: 7 additions & 20 deletions src/cascadia/TerminalControl/ControlCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_renderEngine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
_renderer->AddRenderEngine(_renderEngine.get());

// Hook up the warnings callback as early as possible so that we catch everything.
_renderEngine->SetWarningCallback([this](HRESULT hr, wil::zwstring_view parameter) {
_rendererWarning(hr, parameter);
});

// Initialize our font with the renderer
// We don't have to care about DPI. We'll get a change message immediately if it's not 96
// and react accordingly.
Expand Down Expand Up @@ -371,12 +376,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation

_terminal->CreateFromSettings(*_settings, *_renderer);

// IMPORTANT! Set this callback up sooner than later. If we do it
// after Enable, then it'll be possible to paint the frame once
// _before_ the warning handler is set up, and then warnings from
// the first paint will be ignored!
_renderEngine->SetWarningCallback(std::bind(&ControlCore::_rendererWarning, this, std::placeholders::_1));

// Tell the render engine to notify us when the swap chain changes.
// We do this after we initially set the swapchain so as to avoid
// unnecessary callbacks (and locking problems)
Expand Down Expand Up @@ -1024,18 +1023,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
LOG_IF_FAILED(_renderEngine->UpdateFont(_desiredFont, _actualFont, featureMap, axesMap));
}

// If the actual font isn't what was requested...
if (_actualFont.GetFaceName() != _desiredFont.GetFaceName())
{
// Then warn the user that we picked something because we couldn't find their font.
// Format message with user's choice of font and the font that was chosen instead.
const winrt::hstring message{ fmt::format(std::wstring_view{ RS_(L"NoticeFontNotFound") },
_desiredFont.GetFaceName(),
_actualFont.GetFaceName()) };
auto noticeArgs = winrt::make<NoticeEventArgs>(NoticeLevel::Warning, message);
RaiseNotice.raise(*this, std::move(noticeArgs));
}

const auto actualNewSize = _actualFont.GetSize();
FontSizeChanged.raise(*this, winrt::make<FontSizeChangedArgs>(actualNewSize.width, actualNewSize.height));
}
Expand Down Expand Up @@ -1724,9 +1711,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
}
}

void ControlCore::_rendererWarning(const HRESULT hr)
void ControlCore::_rendererWarning(const HRESULT hr, wil::zwstring_view parameter)
{
RendererWarning.raise(*this, winrt::make<RendererWarningArgs>(hr));
RendererWarning.raise(*this, winrt::make<RendererWarningArgs>(hr, winrt::hstring{ parameter }));
}

winrt::fire_and_forget ControlCore::_renderEngineSwapChainChanged(const HANDLE sourceHandle)
Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/TerminalControl/ControlCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
winrt::Windows::System::DispatcherQueueTimer _midiAudioSkipTimer{ nullptr };

#pragma region RendererCallbacks
void _rendererWarning(const HRESULT hr);
void _rendererWarning(const HRESULT hr, wil::zwstring_view parameter);
winrt::fire_and_forget _renderEngineSwapChainChanged(const HANDLE handle);
void _rendererBackgroundColorChanged();
void _rendererTabColorChanged();
Expand Down
8 changes: 5 additions & 3 deletions src/cascadia/TerminalControl/EventArgs.h
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation
struct RendererWarningArgs : public RendererWarningArgsT<RendererWarningArgs>
{
public:
RendererWarningArgs(const uint64_t hr) :
_Result(hr)
RendererWarningArgs(const HRESULT hr, winrt::hstring parameter) :
_Result{ hr },
_Parameter{ std::move(parameter) }
{
}

WINRT_PROPERTY(uint64_t, Result);
WINRT_PROPERTY(HRESULT, Result);
WINRT_PROPERTY(winrt::hstring, Parameter);
};

struct TransparencyChangedEventArgs : public TransparencyChangedEventArgsT<TransparencyChangedEventArgs>
Expand Down
3 changes: 2 additions & 1 deletion src/cascadia/TerminalControl/EventArgs.idl
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ namespace Microsoft.Terminal.Control

runtimeclass RendererWarningArgs
{
UInt64 Result { get; };
HRESULT Result { get; };
String Parameter { get; };
}

runtimeclass TransparencyChangedEventArgs
Expand Down
8 changes: 8 additions & 0 deletions src/cascadia/TerminalControl/Resources/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,14 @@ Please either install the missing font or choose another one.</value>
<value>Renderer encountered an unexpected error: {0}</value>
<comment>{0} is an error code.</comment>
</data>
<data name="RendererErrorFontNotFound" xml:space="preserve">
<value>Unable to find the following fonts: {0}. Please either install them or choose different fonts.</value>
<comment>{Locked="{0}"} This is a warning dialog shown when the user selects a font that isn't installed.</comment>
</data>
<data name="RendererErrorOther" xml:space="preserve">
<value>Renderer encountered an unexpected error: {0:#010x} {1}</value>
<comment>{Locked="{0:#010x}","{1}"} {0:#010x} is a placeholder for a Windows error code (e.g. 0x88985002). {1} is the corresponding message.</comment>
</data>
<data name="TermControlReadOnly" xml:space="preserve">
<value>Read-only mode is enabled.</value>
</data>
Expand Down
55 changes: 32 additions & 23 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -991,36 +991,45 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// - hr: an HRESULT describing the warning
// Return Value:
// - <none>
winrt::fire_and_forget TermControl::_RendererWarning(IInspectable /*sender*/,
Control::RendererWarningArgs args)
winrt::fire_and_forget TermControl::_RendererWarning(IInspectable /*sender*/, Control::RendererWarningArgs args)
{
const auto hr = static_cast<HRESULT>(args.Result());

auto weakThis{ get_weak() };
co_await wil::resume_foreground(Dispatcher());

if (auto control{ weakThis.get() })
const auto control = weakThis.get();
if (!control)
{
winrt::hstring message;
if (HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) == hr ||
HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND) == hr)
{
message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PixelShaderNotFound") },
(_focused ? _core.FocusedAppearance() : _core.UnfocusedAppearance()).PixelShaderPath()) };
}
else if (D2DERR_SHADER_COMPILE_FAILED == hr)
{
message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PixelShaderCompileFailed") }) };
}
else
{
message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"UnexpectedRendererError") },
hr) };
}
co_return;
}

auto noticeArgs = winrt::make<NoticeEventArgs>(NoticeLevel::Warning, std::move(message));
control->RaiseNotice.raise(*control, std::move(noticeArgs));
const auto hr = args.Result();
const auto parameter = args.Parameter();
winrt::hstring message;

switch (hr)
{
case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND):
case HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND):
message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PixelShaderNotFound") }, parameter) };
break;
case D2DERR_SHADER_COMPILE_FAILED:
message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PixelShaderCompileFailed") }) };
break;
case DWRITE_E_NOFONT:
message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"RendererErrorFontNotFound") }, parameter) };
break;
default:
{
wchar_t buf[512];
const auto len = FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, hr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), &buf[0], ARRAYSIZE(buf), nullptr);
const std::wstring_view msg{ &buf[0], len };
message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"RendererErrorOther") }, hr, msg) };
break;
}
}

auto noticeArgs = winrt::make<NoticeEventArgs>(NoticeLevel::Warning, std::move(message));
control->RaiseNotice.raise(*control, std::move(noticeArgs));
}

void TermControl::_AttachDxgiSwapChainToXaml(HANDLE swapChainHandle)
Expand Down
Loading

0 comments on commit de7f931

Please sign in to comment.