From 4c16cb278ed8b7ba46ff6fd4cf4c48fb095cb1a7 Mon Sep 17 00:00:00 2001 From: PankajBhojwani Date: Thu, 22 Jul 2021 16:15:44 -0700 Subject: [PATCH] Allow users to set font features and font axes (#10525) Adds support for users to be able to set font features and axes (see the spec for more details!) ## Detailed Description **CustomTextLayout** - Asks the `DxFontRenderData` for the font features when getting glyphs - _If any features have been set/updated, we always skip the "isTextSimple" shortcut_ - Asks the `_formatInUse` for any font axes when mapping characters in `_AnalyzeFontFallback` **DxFontRenderData** - Stores a map of font features (initialized to the [standard feature list]) - Stores a map of font axes - Has methods to add font features/axes to the map or update existing ones - Has methods to retrieve the font features/axes - Sets the font axes in the `IDWriteTextFormat` when creating it ## Validation Steps Performed It works! [standard feature list]: https://github.com/fdwr/TextLayoutSampler/blob/ac5aef67d1cc0cb67c5e3be29b30bda5a90c3e2b/DrawableObject.ixx#L802 Specified in #10457 Related to #1790 Closes #759 Closes #5828 --- .github/actions/spelling/allow/allow.txt | 8 + src/cascadia/TerminalControl/ControlCore.cpp | 57 +++-- .../TerminalControl/IControlSettings.idl | 2 + .../CascadiaSettings.cpp | 2 + .../TerminalSettingsModel/FontConfig.cpp | 8 + .../TerminalSettingsModel/FontConfig.h | 5 + .../TerminalSettingsModel/FontConfig.idl | 5 + .../TerminalSettingsModel/JsonUtils.h | 104 +++++++++ .../TerminalSettings.cpp | 2 + .../TerminalSettingsModel/TerminalSettings.h | 5 + .../UnitTests_Control/MockControlSettings.h | 6 + src/renderer/dx/CustomTextLayout.cpp | 121 ++++++---- src/renderer/dx/CustomTextLayout.h | 2 +- src/renderer/dx/DxFontRenderData.cpp | 209 +++++++++++++++++- src/renderer/dx/DxFontRenderData.h | 33 ++- src/renderer/dx/DxRenderer.cpp | 17 +- src/renderer/dx/DxRenderer.hpp | 1 + 17 files changed, 529 insertions(+), 58 deletions(-) diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index d361ba3e4..4ca9009f8 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -1,6 +1,9 @@ apc +calt +ccmp Apc clickable +clig copyable dalet dcs @@ -14,6 +17,7 @@ dzhe Enum'd formattings ftp +fvar geeksforgeeks ghe gje @@ -27,7 +31,9 @@ It'd kje liga lje +locl maxed +mkmk mru nje ogonek @@ -37,10 +43,12 @@ postmodern ptys qof qps +rclt reimplementation reserialization reserialize reserializes +rlig runtimes shcha slnt diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 27724fa7c..bf106322a 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -155,6 +155,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Set up the DX Engine auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); _renderer->AddRenderEngine(dxEngine.get()); + _renderEngine = std::move(dxEngine); // 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 @@ -168,12 +169,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Then, using the font, get the number of characters that can fit. // Resize our terminal connection to match that size, and initialize the terminal with that size. const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize); - LOG_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() })); + LOG_IF_FAILED(_renderEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() })); // Update DxEngine's SelectionBackground - dxEngine->SetSelectionBackground(til::color{ _settings.SelectionBackground() }); + _renderEngine->SetSelectionBackground(til::color{ _settings.SelectionBackground() }); - const auto vp = dxEngine->GetViewportInCharacters(viewInPixels); + const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels); const auto width = vp.Width(); const auto height = vp.Height(); _connection.Resize(height, width); @@ -188,27 +189,26 @@ namespace winrt::Microsoft::Terminal::Control::implementation // 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! - dxEngine->SetWarningCallback(std::bind(&ControlCore::_rendererWarning, this, std::placeholders::_1)); + _renderEngine->SetWarningCallback(std::bind(&ControlCore::_rendererWarning, this, std::placeholders::_1)); // Tell the DX 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) - dxEngine->SetCallback(std::bind(&ControlCore::_renderEngineSwapChainChanged, this)); + _renderEngine->SetCallback(std::bind(&ControlCore::_renderEngineSwapChainChanged, this)); - dxEngine->SetRetroTerminalEffect(_settings.RetroTerminalEffect()); - dxEngine->SetPixelShaderPath(_settings.PixelShaderPath()); - dxEngine->SetForceFullRepaintRendering(_settings.ForceFullRepaintRendering()); - dxEngine->SetSoftwareRendering(_settings.SoftwareRendering()); + _renderEngine->SetRetroTerminalEffect(_settings.RetroTerminalEffect()); + _renderEngine->SetPixelShaderPath(_settings.PixelShaderPath()); + _renderEngine->SetForceFullRepaintRendering(_settings.ForceFullRepaintRendering()); + _renderEngine->SetSoftwareRendering(_settings.SoftwareRendering()); - _updateAntiAliasingMode(dxEngine.get()); + _updateAntiAliasingMode(_renderEngine.get()); // GH#5098: Inform the engine of the opacity of the default text background. if (_settings.UseAcrylic()) { - dxEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.TintOpacity())); + _renderEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.TintOpacity())); } - THROW_IF_FAILED(dxEngine->Enable()); - _renderEngine = std::move(dxEngine); + THROW_IF_FAILED(_renderEngine->Enable()); _initializedTerminal = true; } // scope for TerminalLock @@ -603,9 +603,34 @@ namespace winrt::Microsoft::Terminal::Control::implementation _terminal->SetFontInfo(_actualFont); - // TODO: MSFT:20895307 If the font doesn't exist, this doesn't - // actually fail. We need a way to gracefully fallback. - _renderer->TriggerFontChange(newDpi, _desiredFont, _actualFont); + if (_renderEngine) + { + std::unordered_map featureMap; + if (const auto fontFeatures = _settings.FontFeatures()) + { + featureMap.reserve(fontFeatures.Size()); + + for (const auto& [tag, param] : fontFeatures) + { + featureMap.emplace(tag, param); + } + } + std::unordered_map axesMap; + if (const auto fontAxes = _settings.FontAxes()) + { + axesMap.reserve(fontAxes.Size()); + + for (const auto& [axis, value] : fontAxes) + { + axesMap.emplace(axis, value); + } + } + + // TODO: MSFT:20895307 If the font doesn't exist, this doesn't + // actually fail. We need a way to gracefully fallback. + LOG_IF_FAILED(_renderEngine->UpdateDpi(newDpi)); + LOG_IF_FAILED(_renderEngine->UpdateFont(_desiredFont, _actualFont, featureMap, axesMap)); + } // If the actual font isn't what was requested... if (_actualFont.GetFaceName() != _desiredFont.GetFaceName()) diff --git a/src/cascadia/TerminalControl/IControlSettings.idl b/src/cascadia/TerminalControl/IControlSettings.idl index 1bbc17809..56aab7395 100644 --- a/src/cascadia/TerminalControl/IControlSettings.idl +++ b/src/cascadia/TerminalControl/IControlSettings.idl @@ -36,6 +36,8 @@ namespace Microsoft.Terminal.Control Int32 FontSize; Windows.UI.Text.FontWeight FontWeight; String Padding; + Windows.Foundation.Collections.IMap FontFeatures; + Windows.Foundation.Collections.IMap FontAxes; Microsoft.Terminal.Control.IKeyBindings KeyBindings; diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp index ee685d659..de64a749a 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp @@ -335,6 +335,8 @@ winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::Duplicate DUPLICATE_SETTING_MACRO_SUB(font, target, FontFace); DUPLICATE_SETTING_MACRO_SUB(font, target, FontSize); DUPLICATE_SETTING_MACRO_SUB(font, target, FontWeight); + DUPLICATE_SETTING_MACRO_SUB(font, target, FontFeatures); + DUPLICATE_SETTING_MACRO_SUB(font, target, FontAxes); } { diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.cpp b/src/cascadia/TerminalSettingsModel/FontConfig.cpp index a7c22a581..2d5f1bcd9 100644 --- a/src/cascadia/TerminalSettingsModel/FontConfig.cpp +++ b/src/cascadia/TerminalSettingsModel/FontConfig.cpp @@ -14,6 +14,8 @@ static constexpr std::string_view FontInfoKey{ "font" }; static constexpr std::string_view FontFaceKey{ "face" }; static constexpr std::string_view FontSizeKey{ "size" }; static constexpr std::string_view FontWeightKey{ "weight" }; +static constexpr std::string_view FontFeaturesKey{ "features" }; +static constexpr std::string_view FontAxesKey{ "axes" }; static constexpr std::string_view LegacyFontFaceKey{ "fontFace" }; static constexpr std::string_view LegacyFontSizeKey{ "fontSize" }; static constexpr std::string_view LegacyFontWeightKey{ "fontWeight" }; @@ -29,6 +31,8 @@ winrt::com_ptr FontConfig::CopyFontInfo(const winrt::com_ptr_FontFace = source->_FontFace; fontInfo->_FontSize = source->_FontSize; fontInfo->_FontWeight = source->_FontWeight; + fontInfo->_FontAxes = source->_FontAxes; + fontInfo->_FontFeatures = source->_FontFeatures; return fontInfo; } @@ -39,6 +43,8 @@ Json::Value FontConfig::ToJson() const JsonUtils::SetValueForKey(json, FontFaceKey, _FontFace); JsonUtils::SetValueForKey(json, FontSizeKey, _FontSize); JsonUtils::SetValueForKey(json, FontWeightKey, _FontWeight); + JsonUtils::SetValueForKey(json, FontAxesKey, _FontAxes); + JsonUtils::SetValueForKey(json, FontFeaturesKey, _FontFeatures); return json; } @@ -65,6 +71,8 @@ void FontConfig::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(fontInfoJson, FontFaceKey, _FontFace); JsonUtils::GetValueForKey(fontInfoJson, FontSizeKey, _FontSize); JsonUtils::GetValueForKey(fontInfoJson, FontWeightKey, _FontWeight); + JsonUtils::GetValueForKey(fontInfoJson, FontFeaturesKey, _FontFeatures); + JsonUtils::GetValueForKey(fontInfoJson, FontAxesKey, _FontAxes); } else { diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.h b/src/cascadia/TerminalSettingsModel/FontConfig.h index 5ed2bef3d..61f816dba 100644 --- a/src/cascadia/TerminalSettingsModel/FontConfig.h +++ b/src/cascadia/TerminalSettingsModel/FontConfig.h @@ -23,6 +23,9 @@ Author(s): #include "IInheritable.h" #include +using IFontAxesMap = winrt::Windows::Foundation::Collections::IMap; +using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; + namespace winrt::Microsoft::Terminal::Settings::Model::implementation { struct FontConfig : FontConfigT, IInheritable @@ -39,6 +42,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::FontConfig, hstring, FontFace, DEFAULT_FONT_FACE); INHERITABLE_SETTING(Model::FontConfig, int32_t, FontSize, DEFAULT_FONT_SIZE); INHERITABLE_SETTING(Model::FontConfig, Windows::UI::Text::FontWeight, FontWeight, DEFAULT_FONT_WEIGHT); + INHERITABLE_SETTING(Model::FontConfig, IFontAxesMap, FontAxes); + INHERITABLE_SETTING(Model::FontConfig, IFontFeatureMap, FontFeatures); private: winrt::weak_ref _sourceProfile; diff --git a/src/cascadia/TerminalSettingsModel/FontConfig.idl b/src/cascadia/TerminalSettingsModel/FontConfig.idl index 7bca0ef11..37d2aeb80 100644 --- a/src/cascadia/TerminalSettingsModel/FontConfig.idl +++ b/src/cascadia/TerminalSettingsModel/FontConfig.idl @@ -8,6 +8,8 @@ import "Profile.idl"; _BASE_INHERITABLE_SETTING(Type, Name); \ Microsoft.Terminal.Settings.Model.FontConfig Name##OverrideSource { get; } +#define COMMA , + namespace Microsoft.Terminal.Settings.Model { [default_interface] runtimeclass FontConfig { @@ -16,5 +18,8 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_FONT_SETTING(String, FontFace); INHERITABLE_FONT_SETTING(Int32, FontSize); INHERITABLE_FONT_SETTING(Windows.UI.Text.FontWeight, FontWeight); + + INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap, FontFeatures); + INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap, FontAxes); } } diff --git a/src/cascadia/TerminalSettingsModel/JsonUtils.h b/src/cascadia/TerminalSettingsModel/JsonUtils.h index efb3c0764..fa8dbcb49 100644 --- a/src/cascadia/TerminalSettingsModel/JsonUtils.h +++ b/src/cascadia/TerminalSettingsModel/JsonUtils.h @@ -177,6 +177,58 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils } }; + template + struct ConversionTrait> + { + std::unordered_map FromJson(const Json::Value& json) const + { + std::unordered_map val; + val.reserve(json.size()); + + ConversionTrait trait; + for (auto it = json.begin(), end = json.end(); it != end; ++it) + { + GetValue(*it, val[it.name()], trait); + } + + return val; + } + + bool CanConvert(const Json::Value& json) const + { + if (!json.isObject()) + { + return false; + } + ConversionTrait trait; + for (const auto& v : json) + { + if (!trait.CanConvert(v)) + { + return false; + } + } + return true; + } + + Json::Value ToJson(const std::unordered_map& val) + { + Json::Value json{ Json::objectValue }; + + for (const auto& [k, v] : val) + { + SetValueForKey(json, k, v); + } + + return json; + } + + std::string TypeDescription() const + { + return fmt::format("map (string, {})", ConversionTrait{}.TypeDescription()); + } + }; + #ifdef WINRT_BASE_H template<> struct ConversionTrait : public ConversionTrait @@ -206,6 +258,58 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils return ConversionTrait::CanConvert(json) || json.isNull(); } }; + + template + struct ConversionTrait> + { + winrt::Windows::Foundation::Collections::IMap FromJson(const Json::Value& json) const + { + std::unordered_map val; + val.reserve(json.size()); + + ConversionTrait trait; + for (auto it = json.begin(), end = json.end(); it != end; ++it) + { + GetValue(*it, val[winrt::to_hstring(it.name())], trait); + } + + return winrt::single_threaded_map(std::move(val)); + } + + bool CanConvert(const Json::Value& json) const + { + if (!json.isObject()) + { + return false; + } + ConversionTrait trait; + for (const auto& v : json) + { + if (!trait.CanConvert(v)) + { + return false; + } + } + return true; + } + + Json::Value ToJson(const winrt::Windows::Foundation::Collections::IMap& val) + { + Json::Value json{ Json::objectValue }; + + for (const auto& [k, v] : val) + { + SetValueForKey(json, til::u16u8(k), v); + } + + return json; + } + + std::string TypeDescription() const + { + return fmt::format("map (string, {})", ConversionTrait{}.TypeDescription()); + } + }; #endif template<> diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp index a0ec41a43..124975fb8 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp @@ -279,6 +279,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation _FontFace = profile.FontInfo().FontFace(); _FontSize = profile.FontInfo().FontSize(); _FontWeight = profile.FontInfo().FontWeight(); + _FontFeatures = profile.FontInfo().FontFeatures(); + _FontAxes = profile.FontInfo().FontAxes(); _Padding = profile.Padding(); _Commandline = profile.Commandline(); diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.h b/src/cascadia/TerminalSettingsModel/TerminalSettings.h index 5e87d4151..66e805589 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.h @@ -21,6 +21,9 @@ Author(s): #include #include +using IFontAxesMap = winrt::Windows::Foundation::Collections::IMap; +using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; + // fwdecl unittest classes namespace SettingsModelLocalTests { @@ -123,6 +126,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::TerminalSettings, int32_t, FontSize, DEFAULT_FONT_SIZE); INHERITABLE_SETTING(Model::TerminalSettings, winrt::Windows::UI::Text::FontWeight, FontWeight); + INHERITABLE_SETTING(Model::TerminalSettings, IFontAxesMap, FontAxes); + INHERITABLE_SETTING(Model::TerminalSettings, IFontFeatureMap, FontFeatures); INHERITABLE_SETTING(Model::TerminalSettings, hstring, BackgroundImage); INHERITABLE_SETTING(Model::TerminalSettings, double, BackgroundImageOpacity, 1.0); diff --git a/src/cascadia/UnitTests_Control/MockControlSettings.h b/src/cascadia/UnitTests_Control/MockControlSettings.h index f3c193018..94b8f2a99 100644 --- a/src/cascadia/UnitTests_Control/MockControlSettings.h +++ b/src/cascadia/UnitTests_Control/MockControlSettings.h @@ -8,6 +8,9 @@ Licensed under the MIT license. #include #include +using IFontFeatureMap = winrt::Windows::Foundation::Collections::IMap; +using IFontAxesMap = winrt::Windows::Foundation::Collections::IMap; + namespace ControlUnitTests { class MockControlSettings : public winrt::implements @@ -80,6 +83,9 @@ namespace ControlUnitTests WINRT_PROPERTY(winrt::hstring, PixelShaderPath); + WINRT_PROPERTY(IFontFeatureMap, FontFeatures); + WINRT_PROPERTY(IFontAxesMap, FontAxes); + private: std::array _ColorTable; diff --git a/src/renderer/dx/CustomTextLayout.cpp b/src/renderer/dx/CustomTextLayout.cpp index 0a42ecaea..bf60bce33 100644 --- a/src/renderer/dx/CustomTextLayout.cpp +++ b/src/renderer/dx/CustomTextLayout.cpp @@ -357,7 +357,7 @@ CATCH_RETURN() _glyphIndices.resize(totalGlyphsArrayCount); } - if (_isEntireTextSimple) + if (_isEntireTextSimple && !_fontRenderData->DidUserSetFeatures()) { // When the entire text is simple, we can skip GetGlyphs and directly retrieve glyph indices and // advances(in font design unit). With the help of font metrics, we can calculate the actual glyph @@ -396,10 +396,18 @@ CATCH_RETURN() std::vector textProps(textLength); std::vector glyphProps(maxGlyphCount); + // Get the features to apply to the font + auto features = _fontRenderData->DefaultFontFeatures(); + DWRITE_FONT_FEATURE* featureList = features.data(); + DWRITE_TYPOGRAPHIC_FEATURES typographicFeatures = { &featureList[0], gsl::narrow(features.size()) }; + DWRITE_TYPOGRAPHIC_FEATURES const* typographicFeaturesPointer = &typographicFeatures; + const uint32_t fontFeatureLengths[] = { textLength }; + // Get the glyphs from the text, retrying if needed. int tries = 0; +#pragma warning(suppress : 26485) // so we can pass in the fontFeatureLengths to GetGlyphs without the analyzer complaining HRESULT hr = S_OK; do { @@ -412,9 +420,9 @@ CATCH_RETURN() &run.script, _localeName.data(), (run.isNumberSubstituted) ? _numberSubstitution.Get() : nullptr, - nullptr, // features - nullptr, // featureLengths - 0, // featureCount + &typographicFeaturesPointer, // features + &fontFeatureLengths[0], // featureLengths + 1, // featureCount maxGlyphCount, // maxGlyphCount &_glyphClusters.at(textStart), &textProps.at(0), @@ -462,9 +470,9 @@ CATCH_RETURN() (run.bidiLevel & 1), // isRightToLeft &run.script, _localeName.data(), - nullptr, // features - nullptr, // featureRangeLengths - 0, // featureRanges + &typographicFeaturesPointer, // features + &fontFeatureLengths[0], // featureLengths + 1, // featureCount &_glyphAdvances.at(glyphStart), &_glyphOffsets.at(glyphStart)); @@ -1264,29 +1272,71 @@ CATCH_RETURN(); fallback = _fontRenderData->SystemFontFallback(); } - // Walk through and analyze the entire string - while (textLength > 0) + ::Microsoft::WRL::ComPtr fallback1; + ::Microsoft::WRL::ComPtr format3; + + // If the OS supports IDWriteFontFallback1 and IDWriteTextFormat3, we can use the + // newer MapCharacters to apply axes of variation to the font + if (!FAILED(_formatInUse->QueryInterface(IID_PPV_ARGS(&format3))) && !FAILED(fallback->QueryInterface(IID_PPV_ARGS(&fallback1)))) { - UINT32 mappedLength = 0; - ::Microsoft::WRL::ComPtr mappedFont; - FLOAT scale = 0.0f; + const auto axesVector = _fontRenderData->GetAxisVector(weight, stretch, style, format3.Get()); + // Walk through and analyze the entire string + while (textLength > 0) + { + UINT32 mappedLength = 0; + ::Microsoft::WRL::ComPtr mappedFont; + FLOAT scale = 0.0f; - fallback->MapCharacters(source, - textPosition, - textLength, - collection.Get(), - familyName.data(), - weight, - style, - stretch, - &mappedLength, - &mappedFont, - &scale); + fallback1->MapCharacters(source, + textPosition, + textLength, + collection.Get(), + familyName.data(), + axesVector.data(), + gsl::narrow(axesVector.size()), + &mappedLength, + &scale, + &mappedFont); - RETURN_IF_FAILED(_SetMappedFont(textPosition, mappedLength, mappedFont.Get(), scale)); + RETURN_IF_FAILED(_SetMappedFontFace(textPosition, mappedLength, mappedFont, scale)); - textPosition += mappedLength; - textLength -= mappedLength; + textPosition += mappedLength; + textLength -= mappedLength; + } + } + else + { + // The chunk of code below is very similar to the one above, unfortunately this needs + // to stay for Win7 compatibility reasons. It is also not possible to combine the two + // because they call different versions of MapCharacters + + // Walk through and analyze the entire string + while (textLength > 0) + { + UINT32 mappedLength = 0; + ::Microsoft::WRL::ComPtr mappedFont; + FLOAT scale = 0.0f; + + fallback->MapCharacters(source, + textPosition, + textLength, + collection.Get(), + familyName.data(), + weight, + style, + stretch, + &mappedLength, + &mappedFont, + &scale); + + RETURN_LAST_ERROR_IF(!mappedFont); + ::Microsoft::WRL::ComPtr face; + RETURN_IF_FAILED(mappedFont->CreateFontFace(&face)); + RETURN_IF_FAILED(_SetMappedFontFace(textPosition, mappedLength, face, scale)); + + textPosition += mappedLength; + textLength -= mappedLength; + } } } CATCH_RETURN(); @@ -1300,14 +1350,14 @@ CATCH_RETURN(); // Arguments: // - textPosition - the index to start the substring operation // - textLength - the length of the substring operation -// - font - the font that applies to the substring range +// - fontFace - the fontFace that applies to the substring range // - scale - the scale of the font to apply // Return Value: // - S_OK or appropriate STL/GSL failure code. -[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::_SetMappedFont(UINT32 textPosition, - UINT32 textLength, - _In_ IDWriteFont* const font, - FLOAT const scale) +[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::_SetMappedFontFace(UINT32 textPosition, + UINT32 textLength, + const ::Microsoft::WRL::ComPtr& fontFace, + FLOAT const scale) { try { @@ -1317,14 +1367,9 @@ CATCH_RETURN(); { auto& run = _FetchNextRun(textLength); - if (font != nullptr) + if (fontFace != nullptr) { - // Get font face from font metadata - ::Microsoft::WRL::ComPtr face; - RETURN_IF_FAILED(font->CreateFontFace(&face)); - - // QI for Face5 interface from base face interface, store into run - RETURN_IF_FAILED(face.As(&run.fontFace)); + RETURN_IF_FAILED(fontFace.As(&run.fontFace)); } else { diff --git a/src/renderer/dx/CustomTextLayout.h b/src/renderer/dx/CustomTextLayout.h index 2ac04278b..d1cbabfdb 100644 --- a/src/renderer/dx/CustomTextLayout.h +++ b/src/renderer/dx/CustomTextLayout.h @@ -126,7 +126,7 @@ namespace Microsoft::Console::Render void _OrderRuns(); [[nodiscard]] HRESULT STDMETHODCALLTYPE _AnalyzeFontFallback(IDWriteTextAnalysisSource* const source, UINT32 textPosition, UINT32 textLength); - [[nodiscard]] HRESULT STDMETHODCALLTYPE _SetMappedFont(UINT32 textPosition, UINT32 textLength, IDWriteFont* const font, FLOAT const scale); + [[nodiscard]] HRESULT STDMETHODCALLTYPE _SetMappedFontFace(UINT32 textPosition, UINT32 textLength, const ::Microsoft::WRL::ComPtr& fontFace, FLOAT const scale); [[nodiscard]] HRESULT STDMETHODCALLTYPE _AnalyzeBoxDrawing(gsl::not_null const source, UINT32 textPosition, UINT32 textLength); [[nodiscard]] HRESULT STDMETHODCALLTYPE _SetBoxEffect(UINT32 textPosition, UINT32 textLength); diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp index c91b95dd1..c6b1dbf9b 100644 --- a/src/renderer/dx/DxFontRenderData.cpp +++ b/src/renderer/dx/DxFontRenderData.cpp @@ -12,6 +12,7 @@ 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"; +static constexpr size_t TAG_LENGTH = 4; using namespace Microsoft::Console::Render; @@ -93,6 +94,11 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr return _defaultFontInfo.GetStretch(); } +[[nodiscard]] const std::vector& DxFontRenderData::DefaultFontFeatures() const noexcept +{ + return _featureVector; +} + [[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultTextFormat() { return TextFormatWithAttribute(_defaultFontInfo.GetWeight(), _defaultFontInfo.GetStyle(), _defaultFontInfo.GetStretch()); @@ -178,7 +184,7 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr // - dpi - The DPI of the screen // Return Value: // - S_OK or relevant DirectX error -[[nodiscard]] HRESULT DxFontRenderData::UpdateFont(const FontInfoDesired& desired, FontInfo& actual, const int dpi) noexcept +[[nodiscard]] HRESULT DxFontRenderData::UpdateFont(const FontInfoDesired& desired, FontInfo& actual, const int dpi, const std::unordered_map& features, const std::unordered_map& axes) noexcept { try { @@ -193,6 +199,9 @@ DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwr DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL); + _SetFeatures(features); + _SetAxes(axes); + _BuildFontRenderData(desired, actual, dpi); } CATCH_RETURN(); @@ -441,6 +450,195 @@ try } CATCH_RETURN() +// Routine Description: +// - Returns whether the user set or updated any of the font features to be applied +bool DxFontRenderData::DidUserSetFeatures() const noexcept +{ + return _didUserSetFeatures; +} + +// Routine Description: +// - Updates our internal map of font features with the given features +// - NOTE TO CALLER: Make sure to call _BuildFontRenderData after calling this for the feature changes +// to take place +// Arguments: +// - features - the features to update our map with +void DxFontRenderData::_SetFeatures(const std::unordered_map& features) +{ + // Populate the feature map with the standard list first + std::unordered_map featureMap{ + { DWRITE_MAKE_FONT_FEATURE_TAG('r', 'l', 'i', 'g'), 1 }, // Required Ligatures + { DWRITE_MAKE_FONT_FEATURE_TAG('r', 'c', 'l', 't'), 1 }, // Required Contextual Alternates + { DWRITE_MAKE_FONT_FEATURE_TAG('l', 'o', 'c', 'l'), 1 }, // Localized Forms + { DWRITE_MAKE_FONT_FEATURE_TAG('c', 'c', 'm', 'p'), 1 }, // Glyph Composition / Decomposition + { DWRITE_MAKE_FONT_FEATURE_TAG('c', 'a', 'l', 't'), 1 }, // Contextual Alternates + { DWRITE_MAKE_FONT_FEATURE_TAG('l', 'i', 'g', 'a'), 1 }, // Standard Ligatures + { DWRITE_MAKE_FONT_FEATURE_TAG('c', 'l', 'i', 'g'), 1 }, // Contextual Ligatures + { DWRITE_MAKE_FONT_FEATURE_TAG('k', 'e', 'r', 'n'), 1 }, // Kerning + { DWRITE_MAKE_FONT_FEATURE_TAG('m', 'a', 'r', 'k'), 1 }, // Mark Positioning + { DWRITE_MAKE_FONT_FEATURE_TAG('m', 'k', 'm', 'k'), 1 }, // Mark to Mark Positioning + { DWRITE_MAKE_FONT_FEATURE_TAG('d', 'i', 's', 't'), 1 } // Distances + }; + + // Update our feature map with the provided features + if (!features.empty()) + { + for (const auto [tag, param] : features) + { + if (tag.length() == TAG_LENGTH) + { + featureMap.insert_or_assign(DWRITE_MAKE_FONT_FEATURE_TAG(til::at(tag, 0), til::at(tag, 1), til::at(tag, 2), til::at(tag, 3)), param); + } + } + _didUserSetFeatures = true; + } + else + { + _didUserSetFeatures = false; + } + + // Convert the data to DWRITE_FONT_FEATURE and store it in a vector for CustomTextLayout + _featureVector.clear(); + for (const auto [tag, param] : featureMap) + { + _featureVector.push_back(DWRITE_FONT_FEATURE{ tag, param }); + } +} + +// Routine Description: +// - Updates our internal map of font axes with the given axes +// - NOTE TO CALLER: Make sure to call _BuildFontRenderData after calling this for the axes changes +// to take place +// Arguments: +// - axes - the axes to update our map with +void DxFontRenderData::_SetAxes(const std::unordered_map& axes) +{ + _axesVector.clear(); + + // Update our axis map with the provided axes +#pragma warning(suppress : 26445) // the analyzer doesn't like reference to string_view + for (const auto& [axis, value] : axes) + { + if (axis.length() == TAG_LENGTH) + { + const auto dwriteTag = DWRITE_MAKE_FONT_AXIS_TAG(til::at(axis, 0), til::at(axis, 1), til::at(axis, 2), til::at(axis, 3)); + _axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ dwriteTag, value }); + } + } +} + +// Method Description: +// - Converts a DWRITE_FONT_STRETCH enum into the corresponding float value to +// create a DWRITE_FONT_AXIS_VALUE with +// Arguments: +// - fontStretch: the old DWRITE_FONT_STRETCH enum to be converted into an axis value +// Return value: +// - The float value corresponding to the passed in fontStretch +float DxFontRenderData::_FontStretchToWidthAxisValue(DWRITE_FONT_STRETCH fontStretch) noexcept +{ + // 10 elements from DWRITE_FONT_STRETCH_UNDEFINED (0) to DWRITE_FONT_STRETCH_ULTRA_EXPANDED (9) + static constexpr auto fontStretchEnumToVal = std::array{ 100.0f, 50.0f, 62.5f, 75.0f, 87.5f, 100.0f, 112.5f, 125.0f, 150.0f, 200.0f }; + + if (gsl::narrow_cast(fontStretch) > fontStretchEnumToVal.size()) + { + fontStretch = DWRITE_FONT_STRETCH_NORMAL; + } + + return til::at(fontStretchEnumToVal, fontStretch); +} + +// Method Description: +// - Converts a DWRITE_FONT_STYLE enum into the corresponding float value to +// create a DWRITE_FONT_AXIS_VALUE with +// Arguments: +// - fontStyle: the old DWRITE_FONT_STYLE enum to be converted into an axis value +// Return value: +// - The float value corresponding to the passed in fontStyle +float DxFontRenderData::_FontStyleToSlantFixedAxisValue(DWRITE_FONT_STYLE fontStyle) noexcept +{ + // DWRITE_FONT_STYLE_NORMAL (0), DWRITE_FONT_STYLE_OBLIQUE (1), DWRITE_FONT_STYLE_ITALIC (2) + static constexpr auto fontStyleEnumToVal = std::array{ 0.0f, -20.0f, -12.0f }; + + // Both DWRITE_FONT_STYLE_OBLIQUE and DWRITE_FONT_STYLE_ITALIC default to having slant. + // Though an italic font technically need not have slant (there exist upright ones), the + // vast majority of italic fonts are also slanted. Ideally the slant comes from the + // 'slnt' value in the STAT or fvar table, or the post table italic angle. + + if (gsl::narrow_cast(fontStyle) > fontStyleEnumToVal.size()) + { + fontStyle = DWRITE_FONT_STYLE_NORMAL; + } + + return til::at(fontStyleEnumToVal, fontStyle); +} + +// Method Description: +// - Fill any missing axis values that might be known but were unspecified, such as omitting +// the 'wght' axis tag but specifying the old DWRITE_FONT_WEIGHT enum +// - This function will only be called with a valid IDWriteTextFormat3 +// (on platforms where IDWriteTextFormat3 is supported) +// Arguments: +// - fontWeight: the old DWRITE_FONT_WEIGHT enum to be converted into an axis value +// - fontStretch: the old DWRITE_FONT_STRETCH enum to be converted into an axis value +// - fontStyle: the old DWRITE_FONT_STYLE enum to be converted into an axis value +// - fontSize: the number to convert into an axis value +// - format: the IDWriteTextFormat3 to get the defined axes from +// Return value: +// - The fully formed axes vector +#pragma warning(suppress : 26429) // the analyzer doesn't detect that our FAIL_FAST_IF_NULL macro \ + // checks format for nullness +std::vector DxFontRenderData::GetAxisVector(const DWRITE_FONT_WEIGHT fontWeight, + const DWRITE_FONT_STRETCH fontStretch, + const DWRITE_FONT_STYLE fontStyle, + IDWriteTextFormat3* format) +{ + FAIL_FAST_IF_NULL(format); + + const auto axesCount = format->GetFontAxisValueCount(); + std::vector axesVector; + axesVector.resize(axesCount); + format->GetFontAxisValues(axesVector.data(), axesCount); + + auto axisTagPresence = AxisTagPresence::None; + for (const auto& fontAxisValue : axesVector) + { + switch (fontAxisValue.axisTag) + { + case DWRITE_FONT_AXIS_TAG_WEIGHT: + WI_SetFlag(axisTagPresence, AxisTagPresence::Weight); + break; + case DWRITE_FONT_AXIS_TAG_WIDTH: + WI_SetFlag(axisTagPresence, AxisTagPresence::Width); + break; + case DWRITE_FONT_AXIS_TAG_ITALIC: + WI_SetFlag(axisTagPresence, AxisTagPresence::Italic); + break; + case DWRITE_FONT_AXIS_TAG_SLANT: + WI_SetFlag(axisTagPresence, AxisTagPresence::Slant); + break; + } + } + + if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Weight)) + { + axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_WEIGHT, gsl::narrow(fontWeight) }); + } + if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Width)) + { + axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_WIDTH, _FontStretchToWidthAxisValue(fontStretch) }); + } + if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Italic)) + { + axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_ITALIC, (fontStyle == DWRITE_FONT_STYLE_ITALIC ? 1.0f : 0.0f) }); + } + if (WI_IsFlagClear(axisTagPresence, AxisTagPresence::Slant)) + { + axesVector.emplace_back(DWRITE_FONT_AXIS_VALUE{ DWRITE_FONT_AXIS_TAG_SLANT, _FontStyleToSlantFixedAxisValue(fontStyle) }); + } + + return axesVector; +} + // Routine Description: // - Build the needed data for rendering according to the font used // Arguments: @@ -649,5 +847,14 @@ Microsoft::WRL::ComPtr DxFontRenderData::_BuildTextFormat(con _fontSize, localeName.data(), &format)); + + // If the OS supports IDWriteTextFormat3, set the font axes + ::Microsoft::WRL::ComPtr format3; + if (!_axesVector.empty() && !FAILED(format->QueryInterface(IID_PPV_ARGS(&format3)))) + { + DWRITE_FONT_AXIS_VALUE const* axesList = _axesVector.data(); + format3->SetFontAxisValues(axesList, gsl::narrow(_axesVector.size())); + } + return format; } diff --git a/src/renderer/dx/DxFontRenderData.h b/src/renderer/dx/DxFontRenderData.h index 2cf5fc7be..379fd6036 100644 --- a/src/renderer/dx/DxFontRenderData.h +++ b/src/renderer/dx/DxFontRenderData.h @@ -16,6 +16,16 @@ namespace Microsoft::Console::Render { + enum class AxisTagPresence : BYTE + { + None = 0x00, + Weight = 0x01, + Width = 0x02, + Italic = 0x04, + Slant = 0x08, + }; + DEFINE_ENUM_FLAG_OPERATORS(AxisTagPresence); + class DxFontRenderData { public: @@ -51,6 +61,9 @@ namespace Microsoft::Console::Render // The stretch of default font [[nodiscard]] DWRITE_FONT_STRETCH DefaultFontStretch() noexcept; + // The font features of the default font + [[nodiscard]] const std::vector& DefaultFontFeatures() const noexcept; + // The DirectWrite format object representing the size and other text properties to be applied (by default) [[nodiscard]] Microsoft::WRL::ComPtr DefaultTextFormat(); @@ -70,19 +83,37 @@ namespace Microsoft::Console::Render DWRITE_FONT_STYLE style, DWRITE_FONT_STRETCH stretch); - [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& desired, FontInfo& fiFontInfo, const int dpi) noexcept; + [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& desired, FontInfo& fiFontInfo, const int dpi, const std::unordered_map& features = {}, const std::unordered_map& axes = {}) noexcept; [[nodiscard]] static HRESULT STDMETHODCALLTYPE s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept; + bool DidUserSetFeatures() const noexcept; + + std::vector GetAxisVector(const DWRITE_FONT_WEIGHT fontWeight, + const DWRITE_FONT_STRETCH fontStretch, + const DWRITE_FONT_STYLE fontStyle, + IDWriteTextFormat3* format); + private: using FontAttributeMapKey = uint32_t; + bool _didUserSetFeatures{ false }; + // The font features to apply to the text + std::vector _featureVector; + + // The font axes to apply to the text + std::vector _axesVector; + // We use this to identify font variants with different attributes. static FontAttributeMapKey _ToMapKey(DWRITE_FONT_WEIGHT weight, DWRITE_FONT_STYLE style, DWRITE_FONT_STRETCH stretch) noexcept { return (weight << 16) | (style << 8) | stretch; }; + void _SetFeatures(const std::unordered_map& features); + void _SetAxes(const std::unordered_map& axes); + float _FontStretchToWidthAxisValue(DWRITE_FONT_STRETCH fontStretch) noexcept; + float _FontStyleToSlantFixedAxisValue(DWRITE_FONT_STYLE fontStyle) noexcept; void _BuildFontRenderData(const FontInfoDesired& desired, FontInfo& actual, const int dpi); Microsoft::WRL::ComPtr _BuildTextFormat(const DxFontInfo fontInfo, const std::wstring_view localeName); diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index 03904814e..7748b1556 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -1978,15 +1978,30 @@ CATCH_RETURN() // Routine Description: // - Updates the font used for drawing +// - This is the version that complies with the IRenderEngine interface // Arguments: // - pfiFontInfoDesired - Information specifying the font that is requested // - fiFontInfo - Filled with the nearest font actually chosen for drawing // Return Value: // - S_OK or relevant DirectX error [[nodiscard]] HRESULT DxEngine::UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo) noexcept +{ + return UpdateFont(pfiFontInfoDesired, fiFontInfo, {}, {}); +} + +// Routine Description: +// - Updates the font used for drawing +// Arguments: +// - pfiFontInfoDesired - Information specifying the font that is requested +// - fiFontInfo - Filled with the nearest font actually chosen for drawing +// - features - The map of font features to use +// - axes - The map of font axes to use +// Return Value: +// - S_OK or relevant DirectX error +[[nodiscard]] HRESULT DxEngine::UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept try { - RETURN_IF_FAILED(_fontRenderData->UpdateFont(pfiFontInfoDesired, fiFontInfo, _dpi)); + RETURN_IF_FAILED(_fontRenderData->UpdateFont(pfiFontInfoDesired, fiFontInfo, _dpi, features, axes)); // Prepare the text layout. _customLayout = WRL::Make(_fontRenderData.get()); diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index cbd34ea3b..b74f0ba0f 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -109,6 +109,7 @@ namespace Microsoft::Console::Render const gsl::not_null pData, const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; + [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, const std::unordered_map& features, const std::unordered_map& axes) noexcept; [[nodiscard]] HRESULT UpdateDpi(int const iDpi) noexcept override; [[nodiscard]] HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept override;