diff --git a/src/renderer/base/lib/base.vcxproj b/src/renderer/base/lib/base.vcxproj
index 434f25d2907..f38398cfc9c 100644
--- a/src/renderer/base/lib/base.vcxproj
+++ b/src/renderer/base/lib/base.vcxproj
@@ -6,7 +6,7 @@
base
RendererBase
ConRenderBase
- StaticLibrary
+ StaticLibrary
diff --git a/src/renderer/dx/CustomTextLayout.cpp b/src/renderer/dx/CustomTextLayout.cpp
index 41a677c7bbb..f051026d551 100644
--- a/src/renderer/dx/CustomTextLayout.cpp
+++ b/src/renderer/dx/CustomTextLayout.cpp
@@ -17,43 +17,21 @@ using namespace Microsoft::Console::Render;
// Routine Description:
// - Creates a CustomTextLayout object for calculating which glyphs should be placed and where
// Arguments:
-// - factory - DirectWrite factory reference in case we need other DirectWrite objects for our layout
-// - analyzer - DirectWrite text analyzer from the factory that has been cached at a level above this layout (expensive to create)
-// - format - The DirectWrite format object representing the size and other text properties to be applied (by default) to a layout
-// - formatItalic - The italic variant of the format object representing the size and other text properties for italic text
-// - font - The DirectWrite font face to use while calculating layout (by default, will fallback if necessary)
-// - fontItalic - The italic variant of the font face to use while calculating layout for italic text
-// - width - The count of pixels available per column (the expected pixel width of every column)
-// - boxEffect - Box drawing scaling effects that are cached for the base font across layouts.
-CustomTextLayout::CustomTextLayout(gsl::not_null const factory,
- gsl::not_null const analyzer,
- gsl::not_null const format,
- gsl::not_null const formatItalic,
- gsl::not_null const font,
- gsl::not_null const fontItalic,
- size_t const width,
- IBoxDrawingEffect* const boxEffect) :
- _factory{ factory.get() },
- _analyzer{ analyzer.get() },
- _format{ format.get() },
- _formatItalic{ formatItalic.get() },
- _formatInUse{ format.get() },
- _font{ font.get() },
- _fontItalic{ fontItalic.get() },
- _fontInUse{ font.get() },
- _boxDrawingEffect{ boxEffect },
- _localeName{},
+// - dxFontRenderData - The DirectWrite font render data for our layout
+CustomTextLayout::CustomTextLayout(gsl::not_null const fontRenderData) :
+ _fontRenderData{ fontRenderData },
+ _formatInUse{ fontRenderData->DefaultTextFormat().Get() },
+ _fontInUse{ fontRenderData->DefaultFontFace().Get() },
_numberSubstitution{},
_readingDirection{ DWRITE_READING_DIRECTION_LEFT_TO_RIGHT },
_runs{},
_breakpoints{},
_runIndex{ 0 },
- _width{ width },
+ _width{ gsl::narrow_cast(fontRenderData->GlyphCell().width()) },
_isEntireTextSimple{ false }
{
- // Fetch the locale name out once now from the format
- _localeName.resize(gsl::narrow_cast(format->GetLocaleNameLength()) + 1); // +1 for null
- THROW_IF_FAILED(format->GetLocaleName(_localeName.data(), gsl::narrow(_localeName.size())));
+ _localeName.resize(gsl::narrow_cast(fontRenderData->DefaultTextFormat()->GetLocaleNameLength()) + 1); // +1 for null
+ THROW_IF_FAILED(fontRenderData->DefaultTextFormat()->GetLocaleName(_localeName.data(), gsl::narrow(_localeName.size())));
}
//Routine Description:
@@ -122,8 +100,8 @@ CATCH_RETURN()
RETURN_HR_IF_NULL(E_INVALIDARG, columns);
*columns = 0;
- _formatInUse = _format.Get();
- _fontInUse = _font.Get();
+ _formatInUse = _fontRenderData->DefaultTextFormat().Get();
+ _fontInUse = _fontRenderData->DefaultFontFace().Get();
RETURN_IF_FAILED(_AnalyzeTextComplexity());
RETURN_IF_FAILED(_AnalyzeRuns());
@@ -157,8 +135,8 @@ CATCH_RETURN()
FLOAT originY) noexcept
{
const auto drawingContext = static_cast(clientDrawingContext);
- _formatInUse = drawingContext->useItalicFont ? _formatItalic.Get() : _format.Get();
- _fontInUse = drawingContext->useItalicFont ? _fontItalic.Get() : _font.Get();
+ _formatInUse = drawingContext->useItalicFont ? _fontRenderData->ItalicTextFormat().Get() : _fontRenderData->DefaultTextFormat().Get();
+ _fontInUse = drawingContext->useItalicFont ? _fontRenderData->ItalicFontFace().Get() : _fontRenderData->DefaultFontFace().Get();
RETURN_IF_FAILED(_AnalyzeTextComplexity());
RETURN_IF_FAILED(_AnalyzeRuns());
@@ -196,7 +174,7 @@ CATCH_RETURN()
_glyphIndices.resize(textLength);
- const HRESULT hr = _analyzer->GetTextComplexity(
+ const HRESULT hr = _fontRenderData->Analyzer()->GetTextComplexity(
_text.c_str(),
textLength,
_fontInUse,
@@ -243,10 +221,10 @@ CATCH_RETURN()
if (!_isEntireTextSimple)
{
// Call each of the analyzers in sequence, recording their results.
- RETURN_IF_FAILED(_analyzer->AnalyzeLineBreakpoints(this, 0, textLength, this));
- RETURN_IF_FAILED(_analyzer->AnalyzeBidi(this, 0, textLength, this));
- RETURN_IF_FAILED(_analyzer->AnalyzeScript(this, 0, textLength, this));
- RETURN_IF_FAILED(_analyzer->AnalyzeNumberSubstitution(this, 0, textLength, this));
+ RETURN_IF_FAILED(_fontRenderData->Analyzer()->AnalyzeLineBreakpoints(this, 0, textLength, this));
+ RETURN_IF_FAILED(_fontRenderData->Analyzer()->AnalyzeBidi(this, 0, textLength, this));
+ RETURN_IF_FAILED(_fontRenderData->Analyzer()->AnalyzeScript(this, 0, textLength, this));
+ RETURN_IF_FAILED(_fontRenderData->Analyzer()->AnalyzeNumberSubstitution(this, 0, textLength, this));
// Perform our custom font fallback analyzer that mimics the pattern of the real analyzers.
RETURN_IF_FAILED(_AnalyzeFontFallback(this, 0, textLength));
}
@@ -407,7 +385,7 @@ CATCH_RETURN()
HRESULT hr = S_OK;
do
{
- hr = _analyzer->GetGlyphs(
+ hr = _fontRenderData->Analyzer()->GetGlyphs(
&_text.at(textStart),
textLength,
run.fontFace.Get(),
@@ -452,7 +430,7 @@ CATCH_RETURN()
const auto fontSizeFormat = _formatInUse->GetFontSize();
const auto fontSize = fontSizeFormat * run.fontScale;
- hr = _analyzer->GetGlyphPlacements(
+ hr = _fontRenderData->Analyzer()->GetGlyphPlacements(
&_text.at(textStart),
&_glyphClusters.at(textStart),
&textProps.at(0),
@@ -1265,9 +1243,7 @@ CATCH_RETURN();
if (!fallback)
{
- ::Microsoft::WRL::ComPtr factory2;
- RETURN_IF_FAILED(_factory.As(&factory2));
- factory2->GetSystemFontFallback(&fallback);
+ fallback = _fontRenderData->SystemFontFallback();
}
// Walk through and analyze the entire string
@@ -1467,14 +1443,14 @@ try
{
auto& run = _FetchNextRun(textLength);
- if (run.fontFace == _font)
+ if (run.fontFace == _fontRenderData->DefaultFontFace())
{
- run.drawingEffect = _boxDrawingEffect;
+ run.drawingEffect = _fontRenderData->DefaultBoxDrawingEffect();
}
else
{
::Microsoft::WRL::ComPtr eff;
- RETURN_IF_FAILED(s_CalculateBoxEffect(_formatInUse, _width, run.fontFace.Get(), run.fontScale, &eff));
+ RETURN_IF_FAILED(DxFontRenderData::s_CalculateBoxEffect(_formatInUse, _width, run.fontFace.Get(), run.fontScale, &eff));
// store data in the run
run.drawingEffect = std::move(eff);
@@ -1485,247 +1461,6 @@ try
}
CATCH_RETURN();
-// Routine Description:
-// - Calculates the box drawing scale/translate matrix values to fit a box glyph into the cell as perfectly as possible.
-// Arguments:
-// - format - Text format used to determine line spacing (height including ascent & descent) as calculated from the base font.
-// - widthPixels - The pixel width of the available cell.
-// - face - The font face that is currently being used, may differ from the base font from the layout.
-// - fontScale - if the given font face is going to be scaled versus the format, we need to know so we can compensate for that. pass 1.0f for no scaling.
-// - effect - Receives the effect to apply to box drawing characters. If no effect is received, special treatment isn't required.
-// Return Value:
-// - S_OK, GSL/WIL errors, DirectWrite errors, or math errors.
-[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept
-try
-{
- // Check for bad in parameters.
- RETURN_HR_IF(E_INVALIDARG, !format);
- RETURN_HR_IF(E_INVALIDARG, !face);
-
- // Check the out parameter and fill it up with null.
- RETURN_HR_IF(E_INVALIDARG, !effect);
- *effect = nullptr;
-
- // The format is based around the main font that was specified by the user.
- // We need to know its size as well as the final spacing that was calculated around
- // it when it was first selected to get an idea of how large the bounding box is.
- const auto fontSize = format->GetFontSize();
-
- DWRITE_LINE_SPACING_METHOD spacingMethod;
- float lineSpacing; // total height of the cells
- float baseline; // vertical position counted down from the top where the characters "sit"
- RETURN_IF_FAILED(format->GetLineSpacing(&spacingMethod, &lineSpacing, &baseline));
-
- const float ascentPixels = baseline;
- const float descentPixels = lineSpacing - baseline;
-
- // We need this for the designUnitsPerEm which will be required to move back and forth between
- // Design Units and Pixels. I'll elaborate below.
- DWRITE_FONT_METRICS1 fontMetrics;
- face->GetMetrics(&fontMetrics);
-
- // If we had font fallback occur, the size of the font given to us (IDWriteFontFace1) can be different
- // than the font size used for the original format (IDWriteTextFormat).
- const auto scaledFontSize = fontScale * fontSize;
-
- // This is Unicode FULL BLOCK U+2588.
- // We presume that FULL BLOCK should be filling its entire cell in all directions so it should provide a good basis
- // in knowing exactly where to touch every single edge.
- // We're also presuming that the other box/line drawing glyphs were authored in this font to perfectly inscribe
- // inside of FULL BLOCK, with the same left/top/right/bottom bearings so they would look great when drawn adjacent.
- const UINT32 blockCodepoint = L'\x2588';
-
- // Get the index of the block out of the font.
- UINT16 glyphIndex;
- RETURN_IF_FAILED(face->GetGlyphIndicesW(&blockCodepoint, 1, &glyphIndex));
-
- // If it was 0, it wasn't found in the font. We're going to try again with
- // Unicode BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL U+253C which should be touching
- // all the edges of the possible rectangle, much like a full block should.
- if (glyphIndex == 0)
- {
- const UINT32 alternateCp = L'\x253C';
- RETURN_IF_FAILED(face->GetGlyphIndicesW(&alternateCp, 1, &glyphIndex));
- }
-
- // If we still didn't find the glyph index, we haven't implemented any further logic to figure out the box dimensions.
- // So we're just going to leave successfully as is and apply no scaling factor. It might look not-right, but it won't
- // stop the rendering pipeline.
- RETURN_HR_IF(S_FALSE, glyphIndex == 0);
-
- // Get the metrics of the given glyph, which we're going to treat as the outline box in which all line/block drawing
- // glyphs will be inscribed within, perfectly touching each edge as to align when two cells meet.
- DWRITE_GLYPH_METRICS boxMetrics = { 0 };
- RETURN_IF_FAILED(face->GetDesignGlyphMetrics(&glyphIndex, 1, &boxMetrics));
-
- // NOTE: All metrics we receive from DWRITE are going to be in "design units" which are a somewhat agnostic
- // way of describing proportions.
- // Converting back and forth between real pixels and design units is possible using
- // any font's specific fontSize and the designUnitsPerEm FONT_METRIC value.
- //
- // Here's what to know about the boxMetrics:
- //
- //
- //
- // topLeft --> +--------------------------------+ ---
- // | ^ | |
- // | | topSide | |
- // | | Bearing | |
- // | v | |
- // | +-----------------+ | |
- // | | | | |
- // | | | | | a
- // | | | | | d
- // | | | | | v
- // +<---->+ | | | a
- // | | | | | n
- // | left | | | | c
- // | Side | | | | e
- // | Bea- | | | | H
- // | ring | | right | | e
- // vertical | | | Side | | i
- // OriginY --> x | | Bea- | | g
- // | | | ring | | h
- // | | | | | t
- // | | +<----->+ |
- // | +-----------------+ | |
- // | ^ | |
- // | bottomSide | | |
- // | Bearing | | |
- // | v | |
- // +--------------------------------+ ---
- //
- //
- // | |
- // +--------------------------------+
- // | advanceWidth |
- //
- //
- // NOTE: The bearings can be negative, in which case it is specifying that the glyphs overhang the box
- // as defined by the advanceHeight/width.
- // See also: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics
-
- // The scale is a multiplier and the translation is addition. So *1 and +0 will mean nothing happens.
- const float defaultBoxVerticalScaleFactor = 1.0f;
- float boxVerticalScaleFactor = defaultBoxVerticalScaleFactor;
- const float defaultBoxVerticalTranslation = 0.0f;
- float boxVerticalTranslation = defaultBoxVerticalTranslation;
- {
- // First, find the dimensions of the glyph representing our fully filled box.
-
- // Ascent is how far up from the baseline we'll draw.
- // verticalOriginY is the measure from the topLeft corner of the bounding box down to where
- // the glyph's version of the baseline is.
- // topSideBearing is how much "gap space" is left between that topLeft and where the glyph
- // starts drawing. Subtract the gap space to find how far is drawn upward from baseline.
- const auto boxAscentDesignUnits = boxMetrics.verticalOriginY - boxMetrics.topSideBearing;
-
- // Descent is how far down from the baseline we'll draw.
- // advanceHeight is the total height of the drawn bounding box.
- // verticalOriginY is how much was given to the ascent, so subtract that out.
- // What remains is then the descent value. Remove the
- // bottomSideBearing as the "gap space" on the bottom to find how far is drawn downward from baseline.
- const auto boxDescentDesignUnits = boxMetrics.advanceHeight - boxMetrics.verticalOriginY - boxMetrics.bottomSideBearing;
-
- // The height, then, of the entire box is just the sum of the ascent above the baseline and the descent below.
- const auto boxHeightDesignUnits = boxAscentDesignUnits + boxDescentDesignUnits;
-
- // Second, find the dimensions of the cell we're going to attempt to fit within.
- // We know about the exact ascent/descent units in pixels as calculated when we chose a font and
- // adjusted the ascent/descent for a nice perfect baseline and integer total height.
- // All we need to do is adapt it into Design Units so it meshes nicely with the Design Units above.
- // Use the formula: Pixels * Design Units Per Em / Font Size = Design Units
- const auto cellAscentDesignUnits = ascentPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
- const auto cellDescentDesignUnits = descentPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
- const auto cellHeightDesignUnits = cellAscentDesignUnits + cellDescentDesignUnits;
-
- // OK, now do a few checks. If the drawn box touches the top and bottom of the cell
- // and the box is overall tall enough, then we'll not bother adjusting.
- // We will presume the font author has set things as they wish them to be.
- const auto boxTouchesCellTop = boxAscentDesignUnits >= cellAscentDesignUnits;
- const auto boxTouchesCellBottom = boxDescentDesignUnits >= cellDescentDesignUnits;
- const auto boxIsTallEnoughForCell = boxHeightDesignUnits >= cellHeightDesignUnits;
-
- // If not...
- if (!(boxTouchesCellTop && boxTouchesCellBottom && boxIsTallEnoughForCell))
- {
- // Find a scaling factor that will make the total height drawn of this box
- // perfectly fit the same number of design units as the cell.
- // Since scale factor is a multiplier, it doesn't matter that this is design units.
- // The fraction between the two heights in pixels should be exactly the same
- // (which is what will matter when we go to actually render it... the pixels that is.)
- // Don't scale below 1.0. If it'd shrink, just center it at the prescribed scale.
- boxVerticalScaleFactor = std::max(cellHeightDesignUnits / boxHeightDesignUnits, 1.0f);
-
- // The box as scaled might be hanging over the top or bottom of the cell (or both).
- // We find out the amount of overhang/underhang on both the top and the bottom.
- const auto extraAscent = boxAscentDesignUnits * boxVerticalScaleFactor - cellAscentDesignUnits;
- const auto extraDescent = boxDescentDesignUnits * boxVerticalScaleFactor - cellDescentDesignUnits;
-
- // This took a bit of time and effort and it's difficult to put into words, but here goes.
- // We want the average of the two magnitudes to find out how much to "take" from one and "give"
- // to the other such that both are equal. We presume the glyphs are designed to be drawn
- // centered in their box vertically to look good.
- // The ordering around subtraction is required to ensure that the direction is correct with a negative
- // translation moving up (taking excess descent and adding to ascent) and positive is the opposite.
- const auto boxVerticalTranslationDesignUnits = (extraAscent - extraDescent) / 2;
-
- // The translation is just a raw movement of pixels up or down. Since we were working in Design Units,
- // we need to run the opposite algorithm shown above to go from Design Units to Pixels.
- boxVerticalTranslation = boxVerticalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm;
- }
- }
-
- // The horizontal adjustments follow the exact same logic as the vertical ones.
- const float defaultBoxHorizontalScaleFactor = 1.0f;
- float boxHorizontalScaleFactor = defaultBoxHorizontalScaleFactor;
- const float defaultBoxHorizontalTranslation = 0.0f;
- float boxHorizontalTranslation = defaultBoxHorizontalTranslation;
- {
- // This is the only difference. We don't have a horizontalOriginX from the metrics.
- // However, https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics says
- // the X coordinate is specified by half the advanceWidth to the right of the horizontalOrigin.
- // So we'll use that as the "center" and apply it the role that verticalOriginY had above.
-
- const auto boxCenterDesignUnits = boxMetrics.advanceWidth / 2;
- const auto boxLeftDesignUnits = boxCenterDesignUnits - boxMetrics.leftSideBearing;
- const auto boxRightDesignUnits = boxMetrics.advanceWidth - boxMetrics.rightSideBearing - boxCenterDesignUnits;
- const auto boxWidthDesignUnits = boxLeftDesignUnits + boxRightDesignUnits;
-
- const auto cellWidthDesignUnits = widthPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
- const auto cellLeftDesignUnits = cellWidthDesignUnits / 2;
- const auto cellRightDesignUnits = cellLeftDesignUnits;
-
- const auto boxTouchesCellLeft = boxLeftDesignUnits >= cellLeftDesignUnits;
- const auto boxTouchesCellRight = boxRightDesignUnits >= cellRightDesignUnits;
- const auto boxIsWideEnoughForCell = boxWidthDesignUnits >= cellWidthDesignUnits;
-
- if (!(boxTouchesCellLeft && boxTouchesCellRight && boxIsWideEnoughForCell))
- {
- boxHorizontalScaleFactor = std::max(cellWidthDesignUnits / boxWidthDesignUnits, 1.0f);
- const auto extraLeft = boxLeftDesignUnits * boxHorizontalScaleFactor - cellLeftDesignUnits;
- const auto extraRight = boxRightDesignUnits * boxHorizontalScaleFactor - cellRightDesignUnits;
-
- const auto boxHorizontalTranslationDesignUnits = (extraLeft - extraRight) / 2;
-
- boxHorizontalTranslation = boxHorizontalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm;
- }
- }
-
- // If we set anything, make a drawing effect. Otherwise, there isn't one.
- if (defaultBoxVerticalScaleFactor != boxVerticalScaleFactor ||
- defaultBoxVerticalTranslation != boxVerticalTranslation ||
- defaultBoxHorizontalScaleFactor != boxHorizontalScaleFactor ||
- defaultBoxHorizontalTranslation != boxHorizontalTranslation)
- {
- // OK, make the object that will represent our effect, stuff the metrics into it, and return it.
- RETURN_IF_FAILED(WRL::MakeAndInitialize(effect, boxVerticalScaleFactor, boxVerticalTranslation, boxHorizontalScaleFactor, boxHorizontalTranslation));
- }
-
- return S_OK;
-}
-CATCH_RETURN()
-
#pragma endregion
#pragma region internal Run manipulation functions for storing information from sink callbacks
diff --git a/src/renderer/dx/CustomTextLayout.h b/src/renderer/dx/CustomTextLayout.h
index ca9fd9b9651..2ac04278bf1 100644
--- a/src/renderer/dx/CustomTextLayout.h
+++ b/src/renderer/dx/CustomTextLayout.h
@@ -11,6 +11,7 @@
#include
#include "BoxDrawingEffect.h"
+#include "DxFontRenderData.h"
#include "../inc/Cluster.hpp"
namespace Microsoft::Console::Render
@@ -20,14 +21,7 @@ namespace Microsoft::Console::Render
public:
// Based on the Windows 7 SDK sample at https://github.com/pauldotknopf/WindowsSDK7-Samples/tree/master/multimedia/DirectWrite/CustomLayout
- CustomTextLayout(gsl::not_null const factory,
- gsl::not_null const analyzer,
- gsl::not_null const normalFormat,
- gsl::not_null const italicFormat,
- gsl::not_null const normalFont,
- gsl::not_null const italicFont,
- size_t const width,
- IBoxDrawingEffect* const boxEffect);
+ CustomTextLayout(gsl::not_null const fontRenderData);
[[nodiscard]] HRESULT STDMETHODCALLTYPE AppendClusters(const gsl::span clusters);
@@ -71,8 +65,6 @@ namespace Microsoft::Console::Render
UINT32 textLength,
_In_ IDWriteNumberSubstitution* numberSubstitution) override;
- [[nodiscard]] static HRESULT STDMETHODCALLTYPE s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept;
-
protected:
// A single contiguous run of characters containing the same analysis results.
struct Run
@@ -157,24 +149,15 @@ namespace Microsoft::Console::Render
[[nodiscard]] static constexpr UINT32 _EstimateGlyphCount(const UINT32 textLength) noexcept;
private:
- const ::Microsoft::WRL::ComPtr _factory;
-
- // DirectWrite analyzer
- const ::Microsoft::WRL::ComPtr _analyzer;
+ // DirectWrite font render data
+ DxFontRenderData* _fontRenderData;
// DirectWrite text formats
- const ::Microsoft::WRL::ComPtr _format;
- const ::Microsoft::WRL::ComPtr _formatItalic;
IDWriteTextFormat* _formatInUse;
// DirectWrite font faces
- const ::Microsoft::WRL::ComPtr _font;
- const ::Microsoft::WRL::ComPtr _fontItalic;
IDWriteFontFace1* _fontInUse;
- // Box drawing effect
- const ::Microsoft::WRL::ComPtr _boxDrawingEffect;
-
// The text we're analyzing and processing into a layout
std::wstring _text;
std::vector _textClusterColumns;
diff --git a/src/renderer/dx/DxFontRenderData.cpp b/src/renderer/dx/DxFontRenderData.cpp
new file mode 100644
index 00000000000..0c431f2bf4c
--- /dev/null
+++ b/src/renderer/dx/DxFontRenderData.cpp
@@ -0,0 +1,755 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "precomp.h"
+
+#include "DxFontRenderData.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";
+
+using namespace Microsoft::Console::Render;
+
+DxFontRenderData::DxFontRenderData(::Microsoft::WRL::ComPtr dwriteFactory) noexcept :
+ _dwriteFactory(dwriteFactory),
+ _glyphCell{},
+ _lineMetrics({}),
+ _boxDrawingEffect{}
+{
+}
+
+[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::Analyzer() noexcept
+{
+ return _dwriteTextAnalyzer;
+}
+
+[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::SystemFontFallback()
+{
+ if (!_systemFontFallback)
+ {
+ ::Microsoft::WRL::ComPtr factory2;
+ THROW_IF_FAILED(_dwriteFactory.As(&factory2));
+ factory2->GetSystemFontFallback(&_systemFontFallback);
+ }
+
+ return _systemFontFallback;
+}
+
+[[nodiscard]] til::size DxFontRenderData::GlyphCell() noexcept
+{
+ return _glyphCell;
+}
+
+[[nodiscard]] DxFontRenderData::LineMetrics DxFontRenderData::GetLineMetrics() noexcept
+{
+ return _lineMetrics;
+}
+
+[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultTextFormat() noexcept
+{
+ return _dwriteTextFormat;
+}
+
+[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultFontFace() noexcept
+{
+ return _dwriteFontFace;
+}
+
+[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::DefaultBoxDrawingEffect() noexcept
+{
+ return _boxDrawingEffect;
+}
+
+[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::ItalicTextFormat() noexcept
+{
+ return _dwriteTextFormatItalic;
+}
+
+[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::ItalicFontFace() noexcept
+{
+ return _dwriteFontFaceItalic;
+}
+
+// Routine Description:
+// - Updates the font used for drawing
+// Arguments:
+// - 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:
+// - S_OK or relevant DirectX error
+[[nodiscard]] HRESULT DxFontRenderData::UpdateFont(const FontInfoDesired& desired, FontInfo& actual, const int dpi) noexcept
+{
+ try
+ {
+ _userLocaleName.clear();
+
+ std::wstring fontName(desired.GetFaceName());
+ DWRITE_FONT_WEIGHT weight = static_cast(desired.GetWeight());
+ DWRITE_FONT_STYLE style = DWRITE_FONT_STYLE_NORMAL;
+ DWRITE_FONT_STRETCH stretch = DWRITE_FONT_STRETCH_NORMAL;
+ std::wstring localeName = _GetUserLocaleName();
+
+ // _ResolveFontFaceWithFallback overrides the last argument with the locale name of the font,
+ // but we should use the system's locale to render the text.
+ std::wstring fontLocaleName = localeName;
+ const auto face = _ResolveFontFaceWithFallback(fontName, weight, stretch, style, fontLocaleName);
+
+ DWRITE_FONT_METRICS1 fontMetrics;
+ face->GetMetrics(&fontMetrics);
+
+ const UINT32 spaceCodePoint = L'M';
+ UINT16 spaceGlyphIndex;
+ THROW_IF_FAILED(face->GetGlyphIndicesW(&spaceCodePoint, 1, &spaceGlyphIndex));
+
+ INT32 advanceInDesignUnits;
+ THROW_IF_FAILED(face->GetDesignGlyphAdvances(1, &spaceGlyphIndex, &advanceInDesignUnits));
+
+ DWRITE_GLYPH_METRICS spaceMetrics = { 0 };
+ THROW_IF_FAILED(face->GetDesignGlyphMetrics(&spaceGlyphIndex, 1, &spaceMetrics));
+
+ // The math here is actually:
+ // Requested Size in Points * DPI scaling factor * Points to Pixels scaling factor.
+ // - DPI = dots per inch
+ // - PPI = points per inch or "points" as usually seen when choosing a font size
+ // - The DPI scaling factor is the current monitor DPI divided by 96, the default DPI.
+ // - The Points to Pixels factor is based on the typography definition of 72 points per inch.
+ // As such, converting requires taking the 96 pixel per inch default and dividing by the 72 points per inch
+ // to get a factor of 1 and 1/3.
+ // This turns into something like:
+ // - 12 ppi font * (96 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 16 pixels tall font for 100% display (96 dpi is 100%)
+ // - 12 ppi font * (144 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 24 pixels tall font for 150% display (144 dpi is 150%)
+ // - 12 ppi font * (192 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 32 pixels tall font for 200% display (192 dpi is 200%)
+ float heightDesired = static_cast(desired.GetEngineSize().Y) * static_cast(USER_DEFAULT_SCREEN_DPI) / POINTS_PER_INCH;
+
+ // The advance is the number of pixels left-to-right (X dimension) for the given font.
+ // We're finding a proportional factor here with the design units in "ems", not an actual pixel measurement.
+
+ // Now we play trickery with the font size. Scale by the DPI to get the height we expect.
+ heightDesired *= (static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI));
+
+ const float widthAdvance = static_cast(advanceInDesignUnits) / fontMetrics.designUnitsPerEm;
+
+ // Use the real pixel height desired by the "em" factor for the width to get the number of pixels
+ // we will need per character in width. This will almost certainly result in fractional X-dimension pixels.
+ const float widthApprox = heightDesired * widthAdvance;
+
+ // Since we can't deal with columns of the presentation grid being fractional pixels in width, round to the nearest whole pixel.
+ const float widthExact = round(widthApprox);
+
+ // Now reverse the "em" factor from above to turn the exact pixel width into a (probably) fractional
+ // height in pixels of each character. It's easier for us to pad out height and align vertically
+ // than it is horizontally.
+ const auto fontSize = widthExact / widthAdvance;
+
+ // Now figure out the basic properties of the character height which include ascent and descent
+ // for this specific font size.
+ const float ascent = (fontSize * fontMetrics.ascent) / fontMetrics.designUnitsPerEm;
+ const float descent = (fontSize * fontMetrics.descent) / fontMetrics.designUnitsPerEm;
+
+ // Get the gap.
+ const float gap = (fontSize * fontMetrics.lineGap) / fontMetrics.designUnitsPerEm;
+ const float halfGap = gap / 2;
+
+ // We're going to build a line spacing object here to track all of this data in our format.
+ DWRITE_LINE_SPACING lineSpacing = {};
+ lineSpacing.method = DWRITE_LINE_SPACING_METHOD_UNIFORM;
+
+ // We need to make sure the baseline falls on a round pixel (not a fractional pixel).
+ // If the baseline is fractional, the text appears blurry, especially at small scales.
+ // Since we also need to make sure the bounding box as a whole is round pixels
+ // (because the entire console system maths in full cell units),
+ // we're just going to ceiling up the ascent and descent to make a full pixel amount
+ // and set the baseline to the full round pixel ascent value.
+ //
+ // For reference, for the letters "ag":
+ // ...
+ // gggggg bottom of previous line
+ //
+ // ----------------- <===========================================|
+ // | topSideBearing | 1/2 lineGap |
+ // aaaaaa ggggggg <-------------------------|-------------| |
+ // a g g | | |
+ // aaaaa ggggg |<-ascent | |
+ // a a g | | |---- lineHeight
+ // aaaaa a gggggg <----baseline, verticalOriginY----------|---|
+ // g g |<-descent | |
+ // gggggg <-------------------------|-------------| |
+ // | bottomSideBearing | 1/2 lineGap |
+ // ----------------- <===========================================|
+ //
+ // aaaaaa ggggggg top of next line
+ // ...
+ //
+ // Also note...
+ // We're going to add half the line gap to the ascent and half the line gap to the descent
+ // to ensure that the spacing is balanced vertically.
+ // Generally speaking, the line gap is added to the ascent by DirectWrite itself for
+ // horizontally drawn text which can place the baseline and glyphs "lower" in the drawing
+ // box than would be desired for proper alignment of things like line and box characters
+ // which will try to sit centered in the area and touch perfectly with their neighbors.
+
+ const auto fullPixelAscent = ceil(ascent + halfGap);
+ const auto fullPixelDescent = ceil(descent + halfGap);
+ lineSpacing.height = fullPixelAscent + fullPixelDescent;
+ lineSpacing.baseline = fullPixelAscent;
+
+ // According to MSDN (https://docs.microsoft.com/en-us/windows/win32/api/dwrite_3/ne-dwrite_3-dwrite_font_line_gap_usage)
+ // Setting "ENABLED" means we've included the line gapping in the spacing numbers given.
+ lineSpacing.fontLineGapUsage = DWRITE_FONT_LINE_GAP_USAGE_ENABLED;
+
+ // Create the font with the fractional pixel height size.
+ // It should have an integer pixel width by our math above.
+ // Then below, apply the line spacing to the format to position the floating point pixel height characters
+ // into a cell that has an integer pixel height leaving some padding above/below as necessary to round them out.
+ Microsoft::WRL::ComPtr format;
+ THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontName.data(),
+ nullptr,
+ weight,
+ style,
+ stretch,
+ fontSize,
+ localeName.data(),
+ &format));
+
+ THROW_IF_FAILED(format.As(&_dwriteTextFormat));
+
+ // We also need to create an italic variant of the font face and text
+ // format, based on the same parameters, but using an italic style.
+ std::wstring fontNameItalic = fontName;
+ DWRITE_FONT_WEIGHT weightItalic = weight;
+ DWRITE_FONT_STYLE styleItalic = DWRITE_FONT_STYLE_ITALIC;
+ DWRITE_FONT_STRETCH stretchItalic = stretch;
+
+ const auto faceItalic = _ResolveFontFaceWithFallback(fontNameItalic, weightItalic, stretchItalic, styleItalic, fontLocaleName);
+
+ Microsoft::WRL::ComPtr formatItalic;
+ THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontNameItalic.data(),
+ nullptr,
+ weightItalic,
+ styleItalic,
+ stretchItalic,
+ fontSize,
+ localeName.data(),
+ &formatItalic));
+
+ THROW_IF_FAILED(formatItalic.As(&_dwriteTextFormatItalic));
+
+ Microsoft::WRL::ComPtr analyzer;
+ THROW_IF_FAILED(_dwriteFactory->CreateTextAnalyzer(&analyzer));
+ THROW_IF_FAILED(analyzer.As(&_dwriteTextAnalyzer));
+
+ _dwriteFontFace = face;
+ _dwriteFontFaceItalic = faceItalic;
+
+ THROW_IF_FAILED(_dwriteTextFormat->SetLineSpacing(lineSpacing.method, lineSpacing.height, lineSpacing.baseline));
+ THROW_IF_FAILED(_dwriteTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR));
+ THROW_IF_FAILED(_dwriteTextFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP));
+
+ // The scaled size needs to represent the pixel box that each character will fit within for the purposes
+ // of hit testing math and other such multiplication/division.
+ COORD coordSize = { 0 };
+ coordSize.X = gsl::narrow(widthExact);
+ coordSize.Y = gsl::narrow_cast(lineSpacing.height);
+
+ // Unscaled is for the purposes of re-communicating this font back to the renderer again later.
+ // As such, we need to give the same original size parameter back here without padding
+ // or rounding or scaling manipulation.
+ const COORD unscaled = desired.GetEngineSize();
+
+ const COORD scaled = coordSize;
+
+ actual.SetFromEngine(fontName,
+ desired.GetFamily(),
+ _dwriteTextFormat->GetFontWeight(),
+ false,
+ scaled,
+ unscaled);
+
+ 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));
+ }
+ CATCH_RETURN();
+
+ return S_OK;
+}
+
+// Routine Description:
+// - Calculates the box drawing scale/translate matrix values to fit a box glyph into the cell as perfectly as possible.
+// Arguments:
+// - format - Text format used to determine line spacing (height including ascent & descent) as calculated from the base font.
+// - widthPixels - The pixel width of the available cell.
+// - face - The font face that is currently being used, may differ from the base font from the layout.
+// - fontScale - if the given font face is going to be scaled versus the format, we need to know so we can compensate for that. pass 1.0f for no scaling.
+// - effect - Receives the effect to apply to box drawing characters. If no effect is received, special treatment isn't required.
+// Return Value:
+// - S_OK, GSL/WIL errors, DirectWrite errors, or math errors.
+[[nodiscard]] HRESULT STDMETHODCALLTYPE DxFontRenderData::s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept
+try
+{
+ // Check for bad in parameters.
+ RETURN_HR_IF(E_INVALIDARG, !format);
+ RETURN_HR_IF(E_INVALIDARG, !face);
+
+ // Check the out parameter and fill it up with null.
+ RETURN_HR_IF(E_INVALIDARG, !effect);
+ *effect = nullptr;
+
+ // The format is based around the main font that was specified by the user.
+ // We need to know its size as well as the final spacing that was calculated around
+ // it when it was first selected to get an idea of how large the bounding box is.
+ const auto fontSize = format->GetFontSize();
+
+ DWRITE_LINE_SPACING_METHOD spacingMethod;
+ float lineSpacing; // total height of the cells
+ float baseline; // vertical position counted down from the top where the characters "sit"
+ RETURN_IF_FAILED(format->GetLineSpacing(&spacingMethod, &lineSpacing, &baseline));
+
+ const float ascentPixels = baseline;
+ const float descentPixels = lineSpacing - baseline;
+
+ // We need this for the designUnitsPerEm which will be required to move back and forth between
+ // Design Units and Pixels. I'll elaborate below.
+ DWRITE_FONT_METRICS1 fontMetrics;
+ face->GetMetrics(&fontMetrics);
+
+ // If we had font fallback occur, the size of the font given to us (IDWriteFontFace1) can be different
+ // than the font size used for the original format (IDWriteTextFormat).
+ const auto scaledFontSize = fontScale * fontSize;
+
+ // This is Unicode FULL BLOCK U+2588.
+ // We presume that FULL BLOCK should be filling its entire cell in all directions so it should provide a good basis
+ // in knowing exactly where to touch every single edge.
+ // We're also presuming that the other box/line drawing glyphs were authored in this font to perfectly inscribe
+ // inside of FULL BLOCK, with the same left/top/right/bottom bearings so they would look great when drawn adjacent.
+ const UINT32 blockCodepoint = L'\x2588';
+
+ // Get the index of the block out of the font.
+ UINT16 glyphIndex;
+ RETURN_IF_FAILED(face->GetGlyphIndicesW(&blockCodepoint, 1, &glyphIndex));
+
+ // If it was 0, it wasn't found in the font. We're going to try again with
+ // Unicode BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL U+253C which should be touching
+ // all the edges of the possible rectangle, much like a full block should.
+ if (glyphIndex == 0)
+ {
+ const UINT32 alternateCp = L'\x253C';
+ RETURN_IF_FAILED(face->GetGlyphIndicesW(&alternateCp, 1, &glyphIndex));
+ }
+
+ // If we still didn't find the glyph index, we haven't implemented any further logic to figure out the box dimensions.
+ // So we're just going to leave successfully as is and apply no scaling factor. It might look not-right, but it won't
+ // stop the rendering pipeline.
+ RETURN_HR_IF(S_FALSE, glyphIndex == 0);
+
+ // Get the metrics of the given glyph, which we're going to treat as the outline box in which all line/block drawing
+ // glyphs will be inscribed within, perfectly touching each edge as to align when two cells meet.
+ DWRITE_GLYPH_METRICS boxMetrics = { 0 };
+ RETURN_IF_FAILED(face->GetDesignGlyphMetrics(&glyphIndex, 1, &boxMetrics));
+
+ // NOTE: All metrics we receive from DWRITE are going to be in "design units" which are a somewhat agnostic
+ // way of describing proportions.
+ // Converting back and forth between real pixels and design units is possible using
+ // any font's specific fontSize and the designUnitsPerEm FONT_METRIC value.
+ //
+ // Here's what to know about the boxMetrics:
+ //
+ //
+ //
+ // topLeft --> +--------------------------------+ ---
+ // | ^ | |
+ // | | topSide | |
+ // | | Bearing | |
+ // | v | |
+ // | +-----------------+ | |
+ // | | | | |
+ // | | | | | a
+ // | | | | | d
+ // | | | | | v
+ // +<---->+ | | | a
+ // | | | | | n
+ // | left | | | | c
+ // | Side | | | | e
+ // | Bea- | | | | H
+ // | ring | | right | | e
+ // vertical | | | Side | | i
+ // OriginY --> x | | Bea- | | g
+ // | | | ring | | h
+ // | | | | | t
+ // | | +<----->+ |
+ // | +-----------------+ | |
+ // | ^ | |
+ // | bottomSide | | |
+ // | Bearing | | |
+ // | v | |
+ // +--------------------------------+ ---
+ //
+ //
+ // | |
+ // +--------------------------------+
+ // | advanceWidth |
+ //
+ //
+ // NOTE: The bearings can be negative, in which case it is specifying that the glyphs overhang the box
+ // as defined by the advanceHeight/width.
+ // See also: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics
+
+ // The scale is a multiplier and the translation is addition. So *1 and +0 will mean nothing happens.
+ const float defaultBoxVerticalScaleFactor = 1.0f;
+ float boxVerticalScaleFactor = defaultBoxVerticalScaleFactor;
+ const float defaultBoxVerticalTranslation = 0.0f;
+ float boxVerticalTranslation = defaultBoxVerticalTranslation;
+ {
+ // First, find the dimensions of the glyph representing our fully filled box.
+
+ // Ascent is how far up from the baseline we'll draw.
+ // verticalOriginY is the measure from the topLeft corner of the bounding box down to where
+ // the glyph's version of the baseline is.
+ // topSideBearing is how much "gap space" is left between that topLeft and where the glyph
+ // starts drawing. Subtract the gap space to find how far is drawn upward from baseline.
+ const auto boxAscentDesignUnits = boxMetrics.verticalOriginY - boxMetrics.topSideBearing;
+
+ // Descent is how far down from the baseline we'll draw.
+ // advanceHeight is the total height of the drawn bounding box.
+ // verticalOriginY is how much was given to the ascent, so subtract that out.
+ // What remains is then the descent value. Remove the
+ // bottomSideBearing as the "gap space" on the bottom to find how far is drawn downward from baseline.
+ const auto boxDescentDesignUnits = boxMetrics.advanceHeight - boxMetrics.verticalOriginY - boxMetrics.bottomSideBearing;
+
+ // The height, then, of the entire box is just the sum of the ascent above the baseline and the descent below.
+ const auto boxHeightDesignUnits = boxAscentDesignUnits + boxDescentDesignUnits;
+
+ // Second, find the dimensions of the cell we're going to attempt to fit within.
+ // We know about the exact ascent/descent units in pixels as calculated when we chose a font and
+ // adjusted the ascent/descent for a nice perfect baseline and integer total height.
+ // All we need to do is adapt it into Design Units so it meshes nicely with the Design Units above.
+ // Use the formula: Pixels * Design Units Per Em / Font Size = Design Units
+ const auto cellAscentDesignUnits = ascentPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
+ const auto cellDescentDesignUnits = descentPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
+ const auto cellHeightDesignUnits = cellAscentDesignUnits + cellDescentDesignUnits;
+
+ // OK, now do a few checks. If the drawn box touches the top and bottom of the cell
+ // and the box is overall tall enough, then we'll not bother adjusting.
+ // We will presume the font author has set things as they wish them to be.
+ const auto boxTouchesCellTop = boxAscentDesignUnits >= cellAscentDesignUnits;
+ const auto boxTouchesCellBottom = boxDescentDesignUnits >= cellDescentDesignUnits;
+ const auto boxIsTallEnoughForCell = boxHeightDesignUnits >= cellHeightDesignUnits;
+
+ // If not...
+ if (!(boxTouchesCellTop && boxTouchesCellBottom && boxIsTallEnoughForCell))
+ {
+ // Find a scaling factor that will make the total height drawn of this box
+ // perfectly fit the same number of design units as the cell.
+ // Since scale factor is a multiplier, it doesn't matter that this is design units.
+ // The fraction between the two heights in pixels should be exactly the same
+ // (which is what will matter when we go to actually render it... the pixels that is.)
+ // Don't scale below 1.0. If it'd shrink, just center it at the prescribed scale.
+ boxVerticalScaleFactor = std::max(cellHeightDesignUnits / boxHeightDesignUnits, 1.0f);
+
+ // The box as scaled might be hanging over the top or bottom of the cell (or both).
+ // We find out the amount of overhang/underhang on both the top and the bottom.
+ const auto extraAscent = boxAscentDesignUnits * boxVerticalScaleFactor - cellAscentDesignUnits;
+ const auto extraDescent = boxDescentDesignUnits * boxVerticalScaleFactor - cellDescentDesignUnits;
+
+ // This took a bit of time and effort and it's difficult to put into words, but here goes.
+ // We want the average of the two magnitudes to find out how much to "take" from one and "give"
+ // to the other such that both are equal. We presume the glyphs are designed to be drawn
+ // centered in their box vertically to look good.
+ // The ordering around subtraction is required to ensure that the direction is correct with a negative
+ // translation moving up (taking excess descent and adding to ascent) and positive is the opposite.
+ const auto boxVerticalTranslationDesignUnits = (extraAscent - extraDescent) / 2;
+
+ // The translation is just a raw movement of pixels up or down. Since we were working in Design Units,
+ // we need to run the opposite algorithm shown above to go from Design Units to Pixels.
+ boxVerticalTranslation = boxVerticalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm;
+ }
+ }
+
+ // The horizontal adjustments follow the exact same logic as the vertical ones.
+ const float defaultBoxHorizontalScaleFactor = 1.0f;
+ float boxHorizontalScaleFactor = defaultBoxHorizontalScaleFactor;
+ const float defaultBoxHorizontalTranslation = 0.0f;
+ float boxHorizontalTranslation = defaultBoxHorizontalTranslation;
+ {
+ // This is the only difference. We don't have a horizontalOriginX from the metrics.
+ // However, https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics says
+ // the X coordinate is specified by half the advanceWidth to the right of the horizontalOrigin.
+ // So we'll use that as the "center" and apply it the role that verticalOriginY had above.
+
+ const auto boxCenterDesignUnits = boxMetrics.advanceWidth / 2;
+ const auto boxLeftDesignUnits = boxCenterDesignUnits - boxMetrics.leftSideBearing;
+ const auto boxRightDesignUnits = boxMetrics.advanceWidth - boxMetrics.rightSideBearing - boxCenterDesignUnits;
+ const auto boxWidthDesignUnits = boxLeftDesignUnits + boxRightDesignUnits;
+
+ const auto cellWidthDesignUnits = widthPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
+ const auto cellLeftDesignUnits = cellWidthDesignUnits / 2;
+ const auto cellRightDesignUnits = cellLeftDesignUnits;
+
+ const auto boxTouchesCellLeft = boxLeftDesignUnits >= cellLeftDesignUnits;
+ const auto boxTouchesCellRight = boxRightDesignUnits >= cellRightDesignUnits;
+ const auto boxIsWideEnoughForCell = boxWidthDesignUnits >= cellWidthDesignUnits;
+
+ if (!(boxTouchesCellLeft && boxTouchesCellRight && boxIsWideEnoughForCell))
+ {
+ boxHorizontalScaleFactor = std::max(cellWidthDesignUnits / boxWidthDesignUnits, 1.0f);
+ const auto extraLeft = boxLeftDesignUnits * boxHorizontalScaleFactor - cellLeftDesignUnits;
+ const auto extraRight = boxRightDesignUnits * boxHorizontalScaleFactor - cellRightDesignUnits;
+
+ const auto boxHorizontalTranslationDesignUnits = (extraLeft - extraRight) / 2;
+
+ boxHorizontalTranslation = boxHorizontalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm;
+ }
+ }
+
+ // If we set anything, make a drawing effect. Otherwise, there isn't one.
+ if (defaultBoxVerticalScaleFactor != boxVerticalScaleFactor ||
+ defaultBoxVerticalTranslation != boxVerticalTranslation ||
+ defaultBoxHorizontalScaleFactor != boxHorizontalScaleFactor ||
+ defaultBoxHorizontalTranslation != boxHorizontalTranslation)
+ {
+ // OK, make the object that will represent our effect, stuff the metrics into it, and return it.
+ RETURN_IF_FAILED(WRL::MakeAndInitialize(effect, boxVerticalScaleFactor, boxVerticalTranslation, boxHorizontalScaleFactor, boxHorizontalTranslation));
+ }
+
+ return S_OK;
+}
+CATCH_RETURN()
+
+// Routine Description:
+// - Attempts to locate the font given, but then begins falling back if we cannot find it.
+// - We'll try to fall back to Consolas with the given weight/stretch/style first,
+// then try Consolas again with normal weight/stretch/style,
+// and if nothing works, then we'll throw an error.
+// Arguments:
+// - familyName - The font name we should be looking for
+// - weight - The weight (bold, light, etc.)
+// - stretch - The stretch of the font is the spacing between each letter
+// - style - Normal, italic, etc.
+// Return Value:
+// - Smart pointer holding interface reference for queryable font data.
+[[nodiscard]] Microsoft::WRL::ComPtr DxFontRenderData::_ResolveFontFaceWithFallback(std::wstring& familyName,
+ DWRITE_FONT_WEIGHT& weight,
+ DWRITE_FONT_STRETCH& stretch,
+ DWRITE_FONT_STYLE& style,
+ std::wstring& localeName) const
+{
+ auto face = _FindFontFace(familyName, weight, stretch, style, localeName);
+
+ if (!face)
+ {
+ for (const auto fallbackFace : FALLBACK_FONT_FACES)
+ {
+ familyName = fallbackFace;
+ face = _FindFontFace(familyName, weight, stretch, style, localeName);
+
+ if (face)
+ {
+ 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)
+ {
+ break;
+ }
+ }
+ }
+
+ THROW_HR_IF_NULL(E_FAIL, face);
+
+ return face;
+}
+
+// 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 DxFontRenderData::_FindFontFace(std::wstring& familyName,
+ DWRITE_FONT_WEIGHT& weight,
+ DWRITE_FONT_STRETCH& stretch,
+ DWRITE_FONT_STYLE& style,
+ std::wstring& localeName) const
+{
+ Microsoft::WRL::ComPtr fontFace;
+
+ Microsoft::WRL::ComPtr fontCollection;
+ THROW_IF_FAILED(_dwriteFactory->GetSystemFontCollection(&fontCollection, false));
+
+ UINT32 familyIndex;
+ BOOL familyExists;
+ THROW_IF_FAILED(fontCollection->FindFamilyName(familyName.data(), &familyIndex, &familyExists));
+
+ if (familyExists)
+ {
+ Microsoft::WRL::ComPtr fontFamily;
+ THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily));
+
+ Microsoft::WRL::ComPtr font;
+ THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(weight, stretch, style, &font));
+
+ Microsoft::WRL::ComPtr fontFace0;
+ THROW_IF_FAILED(font->CreateFontFace(&fontFace0));
+
+ THROW_IF_FAILED(fontFace0.As(&fontFace));
+
+ // Retrieve metrics in case the font we created was different than what was requested.
+ weight = font->GetWeight();
+ stretch = font->GetStretch();
+ style = font->GetStyle();
+
+ // Dig the family name out at the end to return it.
+ familyName = _GetFontFamilyName(fontFamily.Get(), localeName);
+ }
+
+ return fontFace;
+}
+
+// Routine Description:
+// - Retrieves the font family name out of the given object in the given locale.
+// - If we can't find a valid name for the given locale, we'll fallback and report it back.
+// Arguments:
+// - fontFamily - DirectWrite font family object
+// - localeName - The locale in which the name should be retrieved.
+// - If fallback occurred, this is updated to what we retrieved instead.
+// Return Value:
+// - Localized string name of the font family
+[[nodiscard]] std::wstring DxFontRenderData::_GetFontFamilyName(gsl::not_null const fontFamily,
+ std::wstring& localeName) const
+{
+ // See: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontcollection
+ Microsoft::WRL::ComPtr familyNames;
+ THROW_IF_FAILED(fontFamily->GetFamilyNames(&familyNames));
+
+ // First we have to find the right family name for the locale. We're going to bias toward what the caller
+ // requested, but fallback if we need to and reply with the locale we ended up choosing.
+ UINT32 index = 0;
+ BOOL exists = false;
+
+ // This returns S_OK whether or not it finds a locale name. Check exists field instead.
+ // If it returns an error, it's a real problem, not an absence of this locale name.
+ // https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-findlocalename
+ THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists));
+
+ // If we tried and it still doesn't exist, try with the fallback locale.
+ if (!exists)
+ {
+ localeName = FALLBACK_LOCALE;
+ THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists));
+ }
+
+ // 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 localeName;
+
+ const auto returnCode = GetUserDefaultLocaleName(localeName.data(), gsl::narrow(localeName.size()));
+ if (returnCode)
+ {
+ _userLocaleName = { localeName.data() };
+ }
+ else
+ {
+ _userLocaleName = { FALLBACK_LOCALE.data(), FALLBACK_LOCALE.size() };
+ }
+ }
+
+ return _userLocaleName;
+}
diff --git a/src/renderer/dx/DxFontRenderData.h b/src/renderer/dx/DxFontRenderData.h
new file mode 100644
index 00000000000..403d323e565
--- /dev/null
+++ b/src/renderer/dx/DxFontRenderData.h
@@ -0,0 +1,96 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+#include "../../renderer/inc/FontInfoDesired.hpp"
+#include "BoxDrawingEffect.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+namespace Microsoft::Console::Render
+{
+ class DxFontRenderData
+ {
+ public:
+ struct LineMetrics
+ {
+ float gridlineWidth;
+ float underlineOffset;
+ float underlineOffset2;
+ float underlineWidth;
+ float strikethroughOffset;
+ float strikethroughWidth;
+ };
+
+ DxFontRenderData(::Microsoft::WRL::ComPtr dwriteFactory) noexcept;
+
+ // DirectWrite text analyzer from the factory
+ [[nodiscard]] Microsoft::WRL::ComPtr Analyzer() noexcept;
+
+ [[nodiscard]] Microsoft::WRL::ComPtr SystemFontFallback();
+
+ [[nodiscard]] til::size GlyphCell() noexcept;
+ [[nodiscard]] LineMetrics GetLineMetrics() noexcept;
+
+ // The DirectWrite format object representing the size and other text properties to be applied (by default)
+ [[nodiscard]] Microsoft::WRL::ComPtr DefaultTextFormat() noexcept;
+
+ // The DirectWrite font face to use while calculating layout (by default)
+ [[nodiscard]] Microsoft::WRL::ComPtr DefaultFontFace() noexcept;
+
+ // Box drawing scaling effects that are cached for the base font across layouts
+ [[nodiscard]] Microsoft::WRL::ComPtr DefaultBoxDrawingEffect() noexcept;
+
+ // The italic variant of the format object representing the size and other text properties for italic text
+ [[nodiscard]] Microsoft::WRL::ComPtr ItalicTextFormat() noexcept;
+
+ // The italic variant of the font face to use while calculating layout for italic text
+ [[nodiscard]] Microsoft::WRL::ComPtr ItalicFontFace() noexcept;
+
+ [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& desired, FontInfo& fiFontInfo, const int dpi) noexcept;
+
+ [[nodiscard]] static HRESULT STDMETHODCALLTYPE s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept;
+
+ private:
+ [[nodiscard]] ::Microsoft::WRL::ComPtr _ResolveFontFaceWithFallback(std::wstring& familyName,
+ DWRITE_FONT_WEIGHT& weight,
+ DWRITE_FONT_STRETCH& stretch,
+ DWRITE_FONT_STYLE& style,
+ std::wstring& localeName) const;
+
+ [[nodiscard]] ::Microsoft::WRL::ComPtr _FindFontFace(std::wstring& familyName,
+ DWRITE_FONT_WEIGHT& weight,
+ DWRITE_FONT_STRETCH& stretch,
+ DWRITE_FONT_STYLE& style,
+ std::wstring& localeName) const;
+
+ [[nodiscard]] std::wstring _GetFontFamilyName(gsl::not_null const fontFamily,
+ std::wstring& localeName) const;
+
+ // A locale that can be used on construction of assorted DX objects that want to know one.
+ [[nodiscard]] std::wstring _GetUserLocaleName();
+
+ ::Microsoft::WRL::ComPtr _dwriteFactory;
+
+ ::Microsoft::WRL::ComPtr _dwriteTextAnalyzer;
+ ::Microsoft::WRL::ComPtr _dwriteTextFormat;
+ ::Microsoft::WRL::ComPtr _dwriteTextFormatItalic;
+ ::Microsoft::WRL::ComPtr _dwriteFontFace;
+ ::Microsoft::WRL::ComPtr _dwriteFontFaceItalic;
+
+ ::Microsoft::WRL::ComPtr _boxDrawingEffect;
+
+ ::Microsoft::WRL::ComPtr _systemFontFallback;
+ std::wstring _userLocaleName;
+
+ til::size _glyphCell;
+
+ LineMetrics _lineMetrics;
+ };
+}
diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp
index a66d312e9f8..1d08a206eea 100644
--- a/src/renderer/dx/DxRenderer.cpp
+++ b/src/renderer/dx/DxRenderer.cpp
@@ -51,10 +51,6 @@ D3D11_INPUT_ELEMENT_DESC _shaderInputLayout[] = {
#pragma hdrstop
-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";
-
using namespace Microsoft::Console::Render;
using namespace Microsoft::Console::Types;
@@ -82,8 +78,6 @@ DxEngine::DxEngine() :
_foregroundColor{ 0 },
_backgroundColor{ 0 },
_selectionBackground{},
- _glyphCell{},
- _boxDrawingEffect{},
_haveDeviceResources{ false },
_swapChainDesc{ 0 },
_swapChainFrameLatencyWaitableObject{ INVALID_HANDLE_VALUE },
@@ -121,6 +115,8 @@ DxEngine::DxEngine() :
// Initialize our default selection color to DEFAULT_FOREGROUND, but make
// sure to set to to a D2D1::ColorF
SetSelectionBackground(DEFAULT_FOREGROUND);
+
+ _fontRenderData = std::make_unique(_dwriteFactory);
}
// Routine Description:
@@ -910,9 +906,9 @@ try
{
return _dwriteFactory->CreateTextLayout(string,
gsl::narrow(stringLength),
- _dwriteTextFormat.Get(),
+ _fontRenderData->DefaultTextFormat().Get(),
_displaySizePixels.width(),
- _glyphCell.height() != 0 ? _glyphCell.height() : _displaySizePixels.height(),
+ _fontRenderData->GlyphCell().height() != 0 ? _fontRenderData->GlyphCell().height() : _displaySizePixels.height(),
ppTextLayout);
}
CATCH_RETURN()
@@ -936,7 +932,7 @@ try
{
_sizeTarget = Pixels;
- _invalidMap.resize(_sizeTarget / _glyphCell, true);
+ _invalidMap.resize(_sizeTarget / _fontRenderData->GlyphCell(), true);
return S_OK;
}
CATCH_RETURN();
@@ -1089,7 +1085,7 @@ try
{
// Dirty client is in pixels. Use divide specialization against glyph factor to make conversion
// to cells.
- _InvalidateRectangle(til::rectangle{ *prcDirtyClient }.scale_down(_glyphCell));
+ _InvalidateRectangle(til::rectangle{ *prcDirtyClient }.scale_down(_fontRenderData->GlyphCell()));
}
return S_OK;
@@ -1312,7 +1308,7 @@ try
{
// Get the baseline for this font as that's where we draw from
DWRITE_LINE_SPACING spacing;
- RETURN_IF_FAILED(_dwriteTextFormat->GetLineSpacing(&spacing.method, &spacing.height, &spacing.baseline));
+ RETURN_IF_FAILED(_fontRenderData->DefaultTextFormat()->GetLineSpacing(&spacing.method, &spacing.height, &spacing.baseline));
// Assemble the drawing context information
_drawingContext = std::make_unique(_d2dDeviceContext.Get(),
@@ -1321,7 +1317,7 @@ try
_ShouldForceGrayscaleAA(),
_dwriteFactory.Get(),
spacing,
- _glyphCell,
+ _fontRenderData->GlyphCell(),
_d2dDeviceContext->GetSize(),
std::nullopt,
D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT);
@@ -1363,14 +1359,14 @@ try
// Scale all dirty rectangles into pixels
std::transform(_presentDirty.begin(), _presentDirty.end(), _presentDirty.begin(), [&](til::rectangle rc) {
- return rc.scale_up(_glyphCell);
+ return rc.scale_up(_fontRenderData->GlyphCell());
});
// Invalid scroll is in characters, convert it to pixels.
- const auto scrollPixels = (_invalidScroll * _glyphCell);
+ const auto scrollPixels = (_invalidScroll * _fontRenderData->GlyphCell());
// The scroll rect is the entire field of cells, but in pixels.
- til::rectangle scrollArea{ _invalidMap.size() * _glyphCell };
+ til::rectangle scrollArea{ _invalidMap.size() * _fontRenderData->GlyphCell() };
// Reduce the size of the rectangle by the scroll.
scrollArea -= til::size{} - scrollPixels;
@@ -1618,7 +1614,7 @@ try
// Runs are counts of cells.
// Use a transform by the size of one cell to convert cells-to-pixels
// as we clear.
- _d2dDeviceContext->SetTransform(D2D1::Matrix3x2F::Scale(_glyphCell));
+ _d2dDeviceContext->SetTransform(D2D1::Matrix3x2F::Scale(_fontRenderData->GlyphCell()));
for (const auto& rect : _invalidMap.runs())
{
// Use aliased.
@@ -1652,7 +1648,7 @@ CATCH_RETURN()
try
{
// Calculate positioning of our origin.
- const D2D1_POINT_2F origin = til::point{ coord } * _glyphCell;
+ const D2D1_POINT_2F origin = til::point{ coord } * _fontRenderData->GlyphCell();
// Create the text layout
RETURN_IF_FAILED(_customLayout->Reset());
@@ -1686,7 +1682,7 @@ try
_d2dBrushForeground->SetColor(_ColorFFromColorRef(color));
- const D2D1_SIZE_F font = _glyphCell;
+ const D2D1_SIZE_F font = _fontRenderData->GlyphCell();
const D2D_POINT_2F target = { coordTarget.X * font.width, coordTarget.Y * font.height };
const auto fullRunWidth = font.width * gsl::narrow_cast(cchLine);
@@ -1701,10 +1697,10 @@ try
// NOTE: Line coordinates are centered within the line, so they need to be
// offset by half the stroke width. For the start coordinate we add half
// the stroke width, and for the end coordinate we subtract half the width.
-
+ const DxFontRenderData::LineMetrics lineMetrics = _fontRenderData->GetLineMetrics();
if (WI_IsAnyFlagSet(lines, (GridLines::Left | GridLines::Right)))
{
- const auto halfGridlineWidth = _lineMetrics.gridlineWidth / 2.0f;
+ const auto halfGridlineWidth = lineMetrics.gridlineWidth / 2.0f;
const auto startY = target.y + halfGridlineWidth;
const auto endY = target.y + font.height - halfGridlineWidth;
@@ -1713,7 +1709,7 @@ try
auto x = target.x + halfGridlineWidth;
for (size_t i = 0; i < cchLine; i++, x += font.width)
{
- DrawLine(x, startY, x, endY, _lineMetrics.gridlineWidth);
+ DrawLine(x, startY, x, endY, lineMetrics.gridlineWidth);
}
}
@@ -1722,27 +1718,27 @@ try
auto x = target.x + font.width - halfGridlineWidth;
for (size_t i = 0; i < cchLine; i++, x += font.width)
{
- DrawLine(x, startY, x, endY, _lineMetrics.gridlineWidth);
+ DrawLine(x, startY, x, endY, lineMetrics.gridlineWidth);
}
}
}
if (WI_IsAnyFlagSet(lines, GridLines::Top | GridLines::Bottom))
{
- const auto halfGridlineWidth = _lineMetrics.gridlineWidth / 2.0f;
+ const auto halfGridlineWidth = lineMetrics.gridlineWidth / 2.0f;
const auto startX = target.x + halfGridlineWidth;
const auto endX = target.x + fullRunWidth - halfGridlineWidth;
if (WI_IsFlagSet(lines, GridLines::Top))
{
const auto y = target.y + halfGridlineWidth;
- DrawLine(startX, y, endX, y, _lineMetrics.gridlineWidth);
+ DrawLine(startX, y, endX, y, lineMetrics.gridlineWidth);
}
if (WI_IsFlagSet(lines, GridLines::Bottom))
{
const auto y = target.y + font.height - halfGridlineWidth;
- DrawLine(startX, y, endX, y, _lineMetrics.gridlineWidth);
+ DrawLine(startX, y, endX, y, lineMetrics.gridlineWidth);
}
}
@@ -1751,37 +1747,37 @@ try
if (WI_IsAnyFlagSet(lines, GridLines::Underline | GridLines::DoubleUnderline | GridLines::HyperlinkUnderline))
{
- const auto halfUnderlineWidth = _lineMetrics.underlineWidth / 2.0f;
+ const auto halfUnderlineWidth = lineMetrics.underlineWidth / 2.0f;
const auto startX = target.x + halfUnderlineWidth;
const auto endX = target.x + fullRunWidth - halfUnderlineWidth;
- const auto y = target.y + _lineMetrics.underlineOffset;
+ const auto y = target.y + lineMetrics.underlineOffset;
if (WI_IsFlagSet(lines, GridLines::Underline))
{
- DrawLine(startX, y, endX, y, _lineMetrics.underlineWidth);
+ DrawLine(startX, y, endX, y, lineMetrics.underlineWidth);
}
if (WI_IsFlagSet(lines, GridLines::HyperlinkUnderline))
{
- DrawHyperlinkLine(startX, y, endX, y, _lineMetrics.underlineWidth);
+ DrawHyperlinkLine(startX, y, endX, y, lineMetrics.underlineWidth);
}
if (WI_IsFlagSet(lines, GridLines::DoubleUnderline))
{
- DrawLine(startX, y, endX, y, _lineMetrics.underlineWidth);
- const auto y2 = target.y + _lineMetrics.underlineOffset2;
- DrawLine(startX, y2, endX, y2, _lineMetrics.underlineWidth);
+ DrawLine(startX, y, endX, y, lineMetrics.underlineWidth);
+ const auto y2 = target.y + lineMetrics.underlineOffset2;
+ DrawLine(startX, y2, endX, y2, lineMetrics.underlineWidth);
}
}
if (WI_IsFlagSet(lines, GridLines::Strikethrough))
{
- const auto halfStrikethroughWidth = _lineMetrics.strikethroughWidth / 2.0f;
+ const auto halfStrikethroughWidth = lineMetrics.strikethroughWidth / 2.0f;
const auto startX = target.x + halfStrikethroughWidth;
const auto endX = target.x + fullRunWidth - halfStrikethroughWidth;
- const auto y = target.y + _lineMetrics.strikethroughOffset;
+ const auto y = target.y + lineMetrics.strikethroughOffset;
- DrawLine(startX, y, endX, y, _lineMetrics.strikethroughWidth);
+ DrawLine(startX, y, endX, y, lineMetrics.strikethroughWidth);
}
return S_OK;
@@ -1805,7 +1801,7 @@ try
_d2dBrushForeground->SetColor(_selectionBackground);
const auto resetColorOnExit = wil::scope_exit([&]() noexcept { _d2dBrushForeground->SetColor(existingColor); });
- const D2D1_RECT_F draw = til::rectangle{ Viewport::FromExclusive(rect).ToInclusive() }.scale_up(_glyphCell);
+ const D2D1_RECT_F draw = til::rectangle{ Viewport::FromExclusive(rect).ToInclusive() }.scale_up(_fontRenderData->GlyphCell());
_d2dDeviceContext->FillRectangle(draw, _d2dBrushForeground.Get());
@@ -1965,30 +1961,10 @@ CATCH_RETURN()
[[nodiscard]] HRESULT DxEngine::UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo) noexcept
try
{
- RETURN_IF_FAILED(_GetProposedFont(pfiFontInfoDesired,
- fiFontInfo,
- _dpi,
- _dwriteTextFormat,
- _dwriteTextFormatItalic,
- _dwriteTextAnalyzer,
- _dwriteFontFace,
- _dwriteFontFaceItalic,
- _lineMetrics));
-
- _glyphCell = fiFontInfo.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(CustomTextLayout::s_CalculateBoxEffect(_dwriteTextFormat.Get(), _glyphCell.width(), _dwriteFontFace.Get(), 1.0f, &_boxDrawingEffect));
+ RETURN_IF_FAILED(_fontRenderData->UpdateFont(pfiFontInfoDesired, fiFontInfo, _dpi));
// Prepare the text layout.
- _customLayout = WRL::Make(_dwriteFactory.Get(),
- _dwriteTextAnalyzer.Get(),
- _dwriteTextFormat.Get(),
- _dwriteTextFormatItalic.Get(),
- _dwriteFontFace.Get(),
- _dwriteFontFaceItalic.Get(),
- _glyphCell.width(),
- _boxDrawingEffect.Get());
+ _customLayout = WRL::Make(_fontRenderData.get());
return S_OK;
}
@@ -1996,16 +1972,16 @@ CATCH_RETURN();
[[nodiscard]] Viewport DxEngine::GetViewportInCharacters(const Viewport& viewInPixels) noexcept
{
- const short widthInChars = base::saturated_cast(viewInPixels.Width() / _glyphCell.width());
- const short heightInChars = base::saturated_cast(viewInPixels.Height() / _glyphCell.height());
+ const short widthInChars = base::saturated_cast(viewInPixels.Width() / _fontRenderData->GlyphCell().width());
+ const short heightInChars = base::saturated_cast(viewInPixels.Height() / _fontRenderData->GlyphCell().height());
return Viewport::FromDimensions(viewInPixels.Origin(), { widthInChars, heightInChars });
}
[[nodiscard]] Viewport DxEngine::GetViewportInPixels(const Viewport& viewInCharacters) noexcept
{
- const short widthInPixels = base::saturated_cast(viewInCharacters.Width() * _glyphCell.width());
- const short heightInPixels = base::saturated_cast(viewInCharacters.Height() * _glyphCell.height());
+ const short widthInPixels = base::saturated_cast(viewInCharacters.Width() * _fontRenderData->GlyphCell().width());
+ const short heightInPixels = base::saturated_cast(viewInCharacters.Height() * _fontRenderData->GlyphCell().height());
return Viewport::FromDimensions(viewInCharacters.Origin(), { widthInPixels, heightInPixels });
}
@@ -2067,22 +2043,8 @@ float DxEngine::GetScaling() const noexcept
FontInfo& pfiFontInfo,
int const iDpi) noexcept
{
- Microsoft::WRL::ComPtr format;
- Microsoft::WRL::ComPtr formatItalic;
- Microsoft::WRL::ComPtr analyzer;
- Microsoft::WRL::ComPtr face;
- Microsoft::WRL::ComPtr faceItalic;
- LineMetrics lineMetrics;
-
- return _GetProposedFont(pfiFontInfoDesired,
- pfiFontInfo,
- iDpi,
- format,
- formatItalic,
- analyzer,
- face,
- faceItalic,
- lineMetrics);
+ DxFontRenderData fontRenderData(_dwriteFactory);
+ return fontRenderData.UpdateFont(pfiFontInfoDesired, pfiFontInfo, iDpi);
}
// Routine Description:
@@ -2108,7 +2070,7 @@ CATCH_RETURN();
[[nodiscard]] HRESULT DxEngine::GetFontSize(_Out_ COORD* const pFontSize) noexcept
try
{
- *pFontSize = _glyphCell;
+ *pFontSize = _fontRenderData->GlyphCell();
return S_OK;
}
CATCH_RETURN();
@@ -2154,447 +2116,6 @@ CATCH_RETURN();
return S_FALSE;
}
-// Routine Description:
-// - Attempts to locate the font given, but then begins falling back if we cannot find it.
-// - We'll try to fall back to Consolas with the given weight/stretch/style first,
-// then try Consolas again with normal weight/stretch/style,
-// and if nothing works, then we'll throw an error.
-// Arguments:
-// - familyName - The font name we should be looking for
-// - weight - The weight (bold, light, etc.)
-// - stretch - The stretch of the font is the spacing between each letter
-// - style - Normal, italic, etc.
-// Return Value:
-// - Smart pointer holding interface reference for queryable font data.
-[[nodiscard]] Microsoft::WRL::ComPtr DxEngine::_ResolveFontFaceWithFallback(std::wstring& familyName,
- DWRITE_FONT_WEIGHT& weight,
- DWRITE_FONT_STRETCH& stretch,
- DWRITE_FONT_STYLE& style,
- std::wstring& localeName) const
-{
- auto face = _FindFontFace(familyName, weight, stretch, style, localeName);
-
- if (!face)
- {
- for (const auto fallbackFace : FALLBACK_FONT_FACES)
- {
- familyName = fallbackFace;
- face = _FindFontFace(familyName, weight, stretch, style, localeName);
-
- if (face)
- {
- 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)
- {
- break;
- }
- }
- }
-
- THROW_HR_IF_NULL(E_FAIL, face);
-
- return face;
-}
-
-// 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 DxEngine::_FindFontFace(std::wstring& familyName,
- DWRITE_FONT_WEIGHT& weight,
- DWRITE_FONT_STRETCH& stretch,
- DWRITE_FONT_STYLE& style,
- std::wstring& localeName) const
-{
- Microsoft::WRL::ComPtr fontFace;
-
- Microsoft::WRL::ComPtr fontCollection;
- THROW_IF_FAILED(_dwriteFactory->GetSystemFontCollection(&fontCollection, false));
-
- UINT32 familyIndex;
- BOOL familyExists;
- THROW_IF_FAILED(fontCollection->FindFamilyName(familyName.data(), &familyIndex, &familyExists));
-
- if (familyExists)
- {
- Microsoft::WRL::ComPtr fontFamily;
- THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily));
-
- Microsoft::WRL::ComPtr font;
- THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(weight, stretch, style, &font));
-
- Microsoft::WRL::ComPtr fontFace0;
- THROW_IF_FAILED(font->CreateFontFace(&fontFace0));
-
- THROW_IF_FAILED(fontFace0.As(&fontFace));
-
- // Retrieve metrics in case the font we created was different than what was requested.
- weight = font->GetWeight();
- stretch = font->GetStretch();
- style = font->GetStyle();
-
- // Dig the family name out at the end to return it.
- familyName = _GetFontFamilyName(fontFamily.Get(), localeName);
- }
-
- return fontFace;
-}
-
-// Routine Description:
-// - Helper to retrieve the user's locale preference or fallback to the default.
-// Arguments:
-// -
-// Return Value:
-// - A locale that can be used on construction of assorted DX objects that want to know one.
-[[nodiscard]] std::wstring DxEngine::_GetLocaleName() const
-{
- std::array localeName;
-
- const auto returnCode = GetUserDefaultLocaleName(localeName.data(), gsl::narrow(localeName.size()));
- if (returnCode)
- {
- return { localeName.data() };
- }
- else
- {
- return { FALLBACK_LOCALE.data(), FALLBACK_LOCALE.size() };
- }
-}
-
-// 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 DxEngine::_GetFontFamilyName(gsl::not_null const fontFamily,
- std::wstring& localeName) const
-{
- // See: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefontcollection
- Microsoft::WRL::ComPtr familyNames;
- THROW_IF_FAILED(fontFamily->GetFamilyNames(&familyNames));
-
- // First we have to find the right family name for the locale. We're going to bias toward what the caller
- // requested, but fallback if we need to and reply with the locale we ended up choosing.
- UINT32 index = 0;
- BOOL exists = false;
-
- // This returns S_OK whether or not it finds a locale name. Check exists field instead.
- // If it returns an error, it's a real problem, not an absence of this locale name.
- // https://docs.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwritelocalizedstrings-findlocalename
- THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists));
-
- // If we tried and it still doesn't exist, try with the fallback locale.
- if (!exists)
- {
- localeName = FALLBACK_LOCALE;
- THROW_IF_FAILED(familyNames->FindLocaleName(localeName.data(), &index, &exists));
- }
-
- // 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:
-// - Updates the font used for drawing
-// Arguments:
-// - 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:
-// - S_OK or relevant DirectX error
-[[nodiscard]] HRESULT DxEngine::_GetProposedFont(const FontInfoDesired& desired,
- FontInfo& actual,
- const int dpi,
- Microsoft::WRL::ComPtr& textFormat,
- Microsoft::WRL::ComPtr& textFormatItalic,
- Microsoft::WRL::ComPtr& textAnalyzer,
- Microsoft::WRL::ComPtr& fontFace,
- Microsoft::WRL::ComPtr& fontFaceItalic,
- LineMetrics& lineMetrics) const noexcept
-{
- try
- {
- std::wstring fontName(desired.GetFaceName());
- DWRITE_FONT_WEIGHT weight = static_cast(desired.GetWeight());
- DWRITE_FONT_STYLE style = DWRITE_FONT_STYLE_NORMAL;
- DWRITE_FONT_STRETCH stretch = DWRITE_FONT_STRETCH_NORMAL;
- std::wstring localeName = _GetLocaleName();
-
- // _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);
-
- DWRITE_FONT_METRICS1 fontMetrics;
- face->GetMetrics(&fontMetrics);
-
- const UINT32 spaceCodePoint = L'M';
- UINT16 spaceGlyphIndex;
- THROW_IF_FAILED(face->GetGlyphIndicesW(&spaceCodePoint, 1, &spaceGlyphIndex));
-
- INT32 advanceInDesignUnits;
- THROW_IF_FAILED(face->GetDesignGlyphAdvances(1, &spaceGlyphIndex, &advanceInDesignUnits));
-
- DWRITE_GLYPH_METRICS spaceMetrics = { 0 };
- THROW_IF_FAILED(face->GetDesignGlyphMetrics(&spaceGlyphIndex, 1, &spaceMetrics));
-
- // The math here is actually:
- // Requested Size in Points * DPI scaling factor * Points to Pixels scaling factor.
- // - DPI = dots per inch
- // - PPI = points per inch or "points" as usually seen when choosing a font size
- // - The DPI scaling factor is the current monitor DPI divided by 96, the default DPI.
- // - The Points to Pixels factor is based on the typography definition of 72 points per inch.
- // As such, converting requires taking the 96 pixel per inch default and dividing by the 72 points per inch
- // to get a factor of 1 and 1/3.
- // This turns into something like:
- // - 12 ppi font * (96 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 16 pixels tall font for 100% display (96 dpi is 100%)
- // - 12 ppi font * (144 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 24 pixels tall font for 150% display (144 dpi is 150%)
- // - 12 ppi font * (192 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 32 pixels tall font for 200% display (192 dpi is 200%)
- float heightDesired = static_cast(desired.GetEngineSize().Y) * static_cast(USER_DEFAULT_SCREEN_DPI) / POINTS_PER_INCH;
-
- // The advance is the number of pixels left-to-right (X dimension) for the given font.
- // We're finding a proportional factor here with the design units in "ems", not an actual pixel measurement.
-
- // Now we play trickery with the font size. Scale by the DPI to get the height we expect.
- heightDesired *= (static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI));
-
- const float widthAdvance = static_cast(advanceInDesignUnits) / fontMetrics.designUnitsPerEm;
-
- // Use the real pixel height desired by the "em" factor for the width to get the number of pixels
- // we will need per character in width. This will almost certainly result in fractional X-dimension pixels.
- const float widthApprox = heightDesired * widthAdvance;
-
- // Since we can't deal with columns of the presentation grid being fractional pixels in width, round to the nearest whole pixel.
- const float widthExact = round(widthApprox);
-
- // Now reverse the "em" factor from above to turn the exact pixel width into a (probably) fractional
- // height in pixels of each character. It's easier for us to pad out height and align vertically
- // than it is horizontally.
- const auto fontSize = widthExact / widthAdvance;
-
- // Now figure out the basic properties of the character height which include ascent and descent
- // for this specific font size.
- const float ascent = (fontSize * fontMetrics.ascent) / fontMetrics.designUnitsPerEm;
- const float descent = (fontSize * fontMetrics.descent) / fontMetrics.designUnitsPerEm;
-
- // Get the gap.
- const float gap = (fontSize * fontMetrics.lineGap) / fontMetrics.designUnitsPerEm;
- const float halfGap = gap / 2;
-
- // We're going to build a line spacing object here to track all of this data in our format.
- DWRITE_LINE_SPACING lineSpacing = {};
- lineSpacing.method = DWRITE_LINE_SPACING_METHOD_UNIFORM;
-
- // We need to make sure the baseline falls on a round pixel (not a fractional pixel).
- // If the baseline is fractional, the text appears blurry, especially at small scales.
- // Since we also need to make sure the bounding box as a whole is round pixels
- // (because the entire console system maths in full cell units),
- // we're just going to ceiling up the ascent and descent to make a full pixel amount
- // and set the baseline to the full round pixel ascent value.
- //
- // For reference, for the letters "ag":
- // ...
- // gggggg bottom of previous line
- //
- // ----------------- <===========================================|
- // | topSideBearing | 1/2 lineGap |
- // aaaaaa ggggggg <-------------------------|-------------| |
- // a g g | | |
- // aaaaa ggggg |<-ascent | |
- // a a g | | |---- lineHeight
- // aaaaa a gggggg <----baseline, verticalOriginY----------|---|
- // g g |<-descent | |
- // gggggg <-------------------------|-------------| |
- // | bottomSideBearing | 1/2 lineGap |
- // ----------------- <===========================================|
- //
- // aaaaaa ggggggg top of next line
- // ...
- //
- // Also note...
- // We're going to add half the line gap to the ascent and half the line gap to the descent
- // to ensure that the spacing is balanced vertically.
- // Generally speaking, the line gap is added to the ascent by DirectWrite itself for
- // horizontally drawn text which can place the baseline and glyphs "lower" in the drawing
- // box than would be desired for proper alignment of things like line and box characters
- // which will try to sit centered in the area and touch perfectly with their neighbors.
-
- const auto fullPixelAscent = ceil(ascent + halfGap);
- const auto fullPixelDescent = ceil(descent + halfGap);
- lineSpacing.height = fullPixelAscent + fullPixelDescent;
- lineSpacing.baseline = fullPixelAscent;
-
- // According to MSDN (https://docs.microsoft.com/en-us/windows/win32/api/dwrite_3/ne-dwrite_3-dwrite_font_line_gap_usage)
- // Setting "ENABLED" means we've included the line gapping in the spacing numbers given.
- lineSpacing.fontLineGapUsage = DWRITE_FONT_LINE_GAP_USAGE_ENABLED;
-
- // Create the font with the fractional pixel height size.
- // It should have an integer pixel width by our math above.
- // Then below, apply the line spacing to the format to position the floating point pixel height characters
- // into a cell that has an integer pixel height leaving some padding above/below as necessary to round them out.
- Microsoft::WRL::ComPtr format;
- THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontName.data(),
- nullptr,
- weight,
- style,
- stretch,
- fontSize,
- localeName.data(),
- &format));
-
- THROW_IF_FAILED(format.As(&textFormat));
-
- // 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;
-
- const auto faceItalic = _ResolveFontFaceWithFallback(fontNameItalic, weightItalic, stretchItalic, styleItalic, fontLocaleName);
-
- Microsoft::WRL::ComPtr formatItalic;
- THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontNameItalic.data(),
- nullptr,
- weightItalic,
- styleItalic,
- stretchItalic,
- fontSize,
- localeName.data(),
- &formatItalic));
-
- THROW_IF_FAILED(formatItalic.As(&textFormatItalic));
-
- Microsoft::WRL::ComPtr analyzer;
- THROW_IF_FAILED(_dwriteFactory->CreateTextAnalyzer(&analyzer));
- THROW_IF_FAILED(analyzer.As(&textAnalyzer));
-
- fontFace = face;
- fontFaceItalic = faceItalic;
-
- 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));
-
- // The scaled size needs to represent the pixel box that each character will fit within for the purposes
- // of hit testing math and other such multiplication/division.
- COORD coordSize = { 0 };
- coordSize.X = gsl::narrow(widthExact);
- coordSize.Y = gsl::narrow_cast(lineSpacing.height);
-
- // Unscaled is for the purposes of re-communicating this font back to the renderer again later.
- // As such, we need to give the same original size parameter back here without padding
- // or rounding or scaling manipulation.
- const COORD unscaled = desired.GetEngineSize();
-
- const COORD scaled = coordSize;
-
- actual.SetFromEngine(fontName,
- desired.GetFamily(),
- textFormat->GetFontWeight(),
- false,
- scaled,
- unscaled);
-
- // 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;
- }
- CATCH_RETURN();
-
- return S_OK;
-}
-
// Routine Description:
// - Helps convert a GDI COLORREF into a Direct2D ColorF
// Arguments:
diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp
index 344dedec3b7..3042c73e9cb 100644
--- a/src/renderer/dx/DxRenderer.hpp
+++ b/src/renderer/dx/DxRenderer.hpp
@@ -26,6 +26,7 @@
#include "CustomTextLayout.h"
#include "CustomTextRenderer.h"
+#include "DxFontRenderData.h"
#include "../../types/inc/Viewport.hpp"
@@ -155,20 +156,7 @@ namespace Microsoft::Console::Render
bool _isEnabled;
bool _isPainting;
- struct LineMetrics
- {
- float gridlineWidth;
- float underlineOffset;
- float underlineOffset2;
- float underlineWidth;
- float strikethroughOffset;
- float strikethroughWidth;
- };
-
- LineMetrics _lineMetrics;
til::size _displaySizePixels;
- til::size _glyphCell;
- ::Microsoft::WRL::ComPtr _boxDrawingEffect;
D2D1_COLOR_F _defaultForegroundColor;
D2D1_COLOR_F _defaultBackgroundColor;
@@ -198,17 +186,14 @@ namespace Microsoft::Console::Render
::Microsoft::WRL::ComPtr _d2dFactory;
::Microsoft::WRL::ComPtr _dwriteFactory;
- ::Microsoft::WRL::ComPtr _dwriteTextFormat;
- ::Microsoft::WRL::ComPtr _dwriteTextFormatItalic;
- ::Microsoft::WRL::ComPtr _dwriteFontFace;
- ::Microsoft::WRL::ComPtr _dwriteFontFaceItalic;
- ::Microsoft::WRL::ComPtr _dwriteTextAnalyzer;
::Microsoft::WRL::ComPtr _customLayout;
::Microsoft::WRL::ComPtr _customRenderer;
::Microsoft::WRL::ComPtr _strokeStyle;
::Microsoft::WRL::ComPtr _dashStrokeStyle;
::Microsoft::WRL::ComPtr _hyperlinkStrokeStyle;
+ std::unique_ptr _fontRenderData;
+
D2D1_STROKE_STYLE_PROPERTIES _strokeStyleProperties;
D2D1_STROKE_STYLE_PROPERTIES _dashStrokeStyleProperties;
@@ -303,33 +288,6 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT _EnableDisplayAccess(const bool outputEnabled) noexcept;
- [[nodiscard]] ::Microsoft::WRL::ComPtr _ResolveFontFaceWithFallback(std::wstring& familyName,
- DWRITE_FONT_WEIGHT& weight,
- DWRITE_FONT_STRETCH& stretch,
- DWRITE_FONT_STYLE& style,
- std::wstring& localeName) const;
-
- [[nodiscard]] ::Microsoft::WRL::ComPtr _FindFontFace(std::wstring& familyName,
- DWRITE_FONT_WEIGHT& weight,
- DWRITE_FONT_STRETCH& stretch,
- DWRITE_FONT_STYLE& style,
- std::wstring& localeName) const;
-
- [[nodiscard]] std::wstring _GetLocaleName() const;
-
- [[nodiscard]] std::wstring _GetFontFamilyName(gsl::not_null const fontFamily,
- std::wstring& localeName) const;
-
- [[nodiscard]] HRESULT _GetProposedFont(const FontInfoDesired& desired,
- FontInfo& actual,
- const int dpi,
- ::Microsoft::WRL::ComPtr& textFormat,
- ::Microsoft::WRL::ComPtr& textFormatItalic,
- ::Microsoft::WRL::ComPtr& textAnalyzer,
- ::Microsoft::WRL::ComPtr& fontFace,
- ::Microsoft::WRL::ComPtr& fontFaceItalic,
- LineMetrics& lineMetrics) const noexcept;
-
[[nodiscard]] til::size _GetClientSize() const;
void _InvalidateRectangle(const til::rectangle& rc);
diff --git a/src/renderer/dx/lib/dx.vcxproj b/src/renderer/dx/lib/dx.vcxproj
index 8eb6ba6e9aa..61f2ca24992 100644
--- a/src/renderer/dx/lib/dx.vcxproj
+++ b/src/renderer/dx/lib/dx.vcxproj
@@ -21,6 +21,7 @@
Create
+
@@ -29,6 +30,7 @@
+
diff --git a/src/renderer/dx/lib/dx.vcxproj.filters b/src/renderer/dx/lib/dx.vcxproj.filters
index 4a0cc0df7dc..86916f13958 100644
--- a/src/renderer/dx/lib/dx.vcxproj.filters
+++ b/src/renderer/dx/lib/dx.vcxproj.filters
@@ -7,6 +7,7 @@
+
@@ -14,6 +15,7 @@
+
diff --git a/src/renderer/dx/sources.inc b/src/renderer/dx/sources.inc
index 34592d022b2..328779d27c4 100644
--- a/src/renderer/dx/sources.inc
+++ b/src/renderer/dx/sources.inc
@@ -33,6 +33,7 @@ INCLUDES = \
SOURCES = \
$(SOURCES) \
..\DxRenderer.cpp \
+ ..\DxFontRenderData.cpp \
..\CustomTextRenderer.cpp \
..\CustomTextLayout.cpp \