diff --git a/.github/actions/spell-check/dictionary/apis.txt b/.github/actions/spell-check/dictionary/apis.txt index c8248141b..0201fff07 100644 --- a/.github/actions/spell-check/dictionary/apis.txt +++ b/.github/actions/spell-check/dictionary/apis.txt @@ -1,3 +1,4 @@ +IBox ICustom IMap IObject @@ -6,6 +7,8 @@ NCHITTEST NCLBUTTONDBLCLK NCRBUTTONDBLCLK NOREDIRECTIONBITMAP +oaidl +ocidl rfind roundf SIZENS diff --git a/.gitignore b/.gitignore index 736808fe9..3db8546ef 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ dlldata.c project.lock.json artifacts/ +*_h.h *_i.c *_p.c *_i.h diff --git a/doc/reference/customtextlayout.xlsx b/doc/reference/customtextlayout.xlsx new file mode 100644 index 000000000..34caeb42f Binary files /dev/null and b/doc/reference/customtextlayout.xlsx differ diff --git a/src/renderer/dx/BoxDrawingEffect.cpp b/src/renderer/dx/BoxDrawingEffect.cpp new file mode 100644 index 000000000..2fa0163f0 --- /dev/null +++ b/src/renderer/dx/BoxDrawingEffect.cpp @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "BoxDrawingEffect.h" + +using namespace Microsoft::Console::Render; + +BoxDrawingEffect::BoxDrawingEffect() noexcept : + _scale{ 1.0f, 0.0f, 1.0f, 0.0f } +{ +} + +#pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize +HRESULT BoxDrawingEffect::RuntimeClassInitialize(float verticalScale, float verticalTranslate, float horizontalScale, float horizontalTranslate) noexcept +{ + _scale.VerticalScale = verticalScale; + _scale.VerticalTranslation = verticalTranslate; + _scale.HorizontalScale = horizontalScale; + _scale.HorizontalTranslation = horizontalTranslate; + return S_OK; +} + +[[nodiscard]] HRESULT STDMETHODCALLTYPE BoxDrawingEffect::GetScale(BoxScale* scale) noexcept +{ + RETURN_HR_IF_NULL(E_INVALIDARG, scale); + *scale = _scale; + return S_OK; +} diff --git a/src/renderer/dx/BoxDrawingEffect.h b/src/renderer/dx/BoxDrawingEffect.h new file mode 100644 index 000000000..1bd0439cf --- /dev/null +++ b/src/renderer/dx/BoxDrawingEffect.h @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include +#include +#include + +#include "IBoxDrawingEffect_h.h" + +namespace Microsoft::Console::Render +{ + class BoxDrawingEffect : public ::Microsoft::WRL::RuntimeClass<::Microsoft::WRL::RuntimeClassFlags<::Microsoft::WRL::ClassicCom | ::Microsoft::WRL::InhibitFtmBase>, IBoxDrawingEffect> + { + public: + BoxDrawingEffect() noexcept; + HRESULT RuntimeClassInitialize(float verticalScale, float verticalTranslate, float horizontalScale, float horizontalTranslate) noexcept; + + [[nodiscard]] HRESULT STDMETHODCALLTYPE GetScale(BoxScale* scale) noexcept override; + + protected: + private: + BoxScale _scale; +#ifdef UNIT_TESTING + public: + friend class BoxDrawingEffectTests; +#endif + }; +} diff --git a/src/renderer/dx/CustomTextLayout.cpp b/src/renderer/dx/CustomTextLayout.cpp index 10f233b21..7593e17d7 100644 --- a/src/renderer/dx/CustomTextLayout.cpp +++ b/src/renderer/dx/CustomTextLayout.cpp @@ -9,6 +9,8 @@ #include #include +#include "BoxDrawingEffect.h" + using namespace Microsoft::Console::Render; // Routine Description: @@ -20,16 +22,19 @@ using namespace Microsoft::Console::Render; // - font - The DirectWrite font face to use while calculating layout (by default, will fallback if necessary) // - clusters - From the backing buffer, the text to be displayed clustered by the columns it should consume. // - 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 font, std::basic_string_view const clusters, - size_t const width) : + size_t const width, + IBoxDrawingEffect* const boxEffect) : _factory{ factory.get() }, _analyzer{ analyzer.get() }, _format{ format.get() }, _font{ font.get() }, + _boxDrawingEffect{ boxEffect }, _localeName{}, _numberSubstitution{}, _readingDirection{ DWRITE_READING_DIRECTION_LEFT_TO_RIGHT }, @@ -104,6 +109,11 @@ CustomTextLayout::CustomTextLayout(gsl::not_null const factory RETURN_IF_FAILED(_AnalyzeRuns()); RETURN_IF_FAILED(_ShapeGlyphRuns()); RETURN_IF_FAILED(_CorrectGlyphRuns()); + // Correcting box drawing has to come after both font fallback and + // the glyph run advance correction (which will apply a font size scaling factor). + // We need to know all the proposed X and Y dimension metrics to get this right. + RETURN_IF_FAILED(_CorrectBoxDrawing()); + RETURN_IF_FAILED(_DrawGlyphRuns(clientDrawingContext, renderer, { originX, originY })); return S_OK; @@ -768,7 +778,7 @@ CATCH_RETURN(); DWRITE_MEASURING_MODE_NATURAL, &glyphRun, &glyphRunDescription, - nullptr)); + run.drawingEffect.Get())); // Either way, we should be at this point by the end of writing this sequence, // whether it was LTR or RTL. @@ -1112,6 +1122,7 @@ CATCH_RETURN(); // - textLength - the length of the substring operation // - font - the font that applies to the substring range // - scale - the scale of the font to apply +// Return Value: // - S_OK or appropriate STL/GSL failure code. [[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::_SetMappedFont(UINT32 textPosition, UINT32 textLength, @@ -1148,6 +1159,386 @@ CATCH_RETURN(); return S_OK; } +#pragma endregion + +#pragma region internal methods for mimicking text analyzer to identify and split box drawing regions + +// Routine Description: +// - Helper method to detect if something is a box drawing character. +// Arguments: +// - wch - Specific character. +// Return Value: +// - True if box drawing. False otherwise. +static constexpr bool _IsBoxDrawingCharacter(const wchar_t wch) +{ + if (wch >= 0x2500 && wch <= 0x259F) + { + return true; + } + + return false; +} + +// Routine Description: +// - Corrects all runs for box drawing characteristics. Splits as it walks, if it must. +// If there are fallback fonts, this must happen after that's analyzed and after the +// advances are corrected so we can use the font size scaling factors to determine +// the appropriate layout heights for the correction scale/translate matrix. +// Arguments: +// - - Operates on all runs then orders them back up. +// Return Value: +// - S_OK, STL/GSL errors, or an E_ABORT from mathematical failures. +[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::_CorrectBoxDrawing() noexcept +try +{ + RETURN_IF_FAILED(_AnalyzeBoxDrawing(this, 0, gsl::narrow(_text.size()))); + _OrderRuns(); + return S_OK; +} +CATCH_RETURN(); + +// Routine Description: +// - An analyzer to walk through the source text and search for runs of box drawing characters. +// It will segment the text into runs of those characters and mark them for special drawing, if necessary. +// Arguments: +// - source - a text analysis source to retrieve substrings of the text to be analyzed +// - textPosition - the index to start the substring operation +// - textLength - the length of the substring operation +// Result: +// - S_OK, STL/GSL errors, or an E_ABORT from mathematical failures. +[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::_AnalyzeBoxDrawing(gsl::not_null const source, + UINT32 textPosition, + UINT32 textLength) +try +{ + // Walk through and analyze the entire string + while (textLength > 0) + { + // Get the substring of text remaining to analyze. + const WCHAR* text; + UINT32 length; + RETURN_IF_FAILED(source->GetTextAtPosition(textPosition, &text, &length)); + + // Put it into a view for iterator convenience. + const std::wstring_view str(text, length); + + // Find the first box drawing character in the string from the front. + const auto firstBox = std::find_if(str.cbegin(), str.cend(), _IsBoxDrawingCharacter); + + // If we found no box drawing characters, move on with life. + if (firstBox == str.cend()) + { + return S_OK; + } + // If we found one, keep looking forward until we find NOT a box drawing character. + else + { + // Find the last box drawing character. + const auto lastBox = std::find_if(firstBox, str.cend(), [](wchar_t wch) { return !_IsBoxDrawingCharacter(wch); }); + + // Skip distance is how far we had to move forward to find a box. + const auto firstBoxDistance = std::distance(str.cbegin(), firstBox); + UINT32 skipDistance; + RETURN_HR_IF(E_ABORT, !base::MakeCheckedNum(firstBoxDistance).AssignIfValid(&skipDistance)); + + // Move the position/length of the outside counters up to the part where boxes start. + textPosition += skipDistance; + textLength -= skipDistance; + + // Run distance is how many box characters in a row there are. + const auto runDistance = std::distance(firstBox, lastBox); + UINT32 mappedLength; + RETURN_HR_IF(E_ABORT, !base::MakeCheckedNum(runDistance).AssignIfValid(&mappedLength)); + + // Split the run and set the box effect on this segment of the run + RETURN_IF_FAILED(_SetBoxEffect(textPosition, mappedLength)); + + // Move us forward for the outer loop to continue scanning after this point. + textPosition += mappedLength; + textLength -= mappedLength; + } + } + + return S_OK; +} +CATCH_RETURN(); + +// Routine Description: +// - A callback to split a run and apply box drawing characteristics to just that sub-run. +// Arguments: +// - textPosition - the index to start the substring operation +// - textLength - the length of the substring operation +// Return Value: +// - S_OK or appropriate STL/GSL failure code. +[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::_SetBoxEffect(UINT32 textPosition, + UINT32 textLength) +try +{ + _SetCurrentRun(textPosition); + _SplitCurrentRun(textPosition); + + while (textLength > 0) + { + auto& run = _FetchNextRun(textLength); + + if (run.fontFace == _font) + { + run.drawingEffect = _boxDrawingEffect; + } + else + { + ::Microsoft::WRL::ComPtr eff; + RETURN_IF_FAILED(s_CalculateBoxEffect(_format.Get(), _width, run.fontFace.Get(), run.fontScale, &eff)); + + // store data in the run + run.drawingEffect = std::move(eff); + } + } + + return S_OK; +} +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 diff --git a/src/renderer/dx/CustomTextLayout.h b/src/renderer/dx/CustomTextLayout.h index 02168b7db..3f4f39b92 100644 --- a/src/renderer/dx/CustomTextLayout.h +++ b/src/renderer/dx/CustomTextLayout.h @@ -10,6 +10,7 @@ #include #include +#include "BoxDrawingEffect.h" #include "../inc/Cluster.hpp" namespace Microsoft::Console::Render @@ -24,7 +25,8 @@ namespace Microsoft::Console::Render gsl::not_null const format, gsl::not_null const font, const std::basic_string_view<::Microsoft::Console::Render::Cluster> clusters, - size_t const width); + size_t const width, + IBoxDrawingEffect* const boxEffect); [[nodiscard]] HRESULT STDMETHODCALLTYPE GetColumns(_Out_ UINT32* columns); @@ -64,6 +66,8 @@ 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 @@ -78,7 +82,8 @@ namespace Microsoft::Console::Render isNumberSubstituted(), isSideways(), fontFace{ nullptr }, - fontScale{ 1.0 } + fontScale{ 1.0 }, + drawingEffect{ nullptr } { } @@ -92,6 +97,7 @@ namespace Microsoft::Console::Render bool isSideways; ::Microsoft::WRL::ComPtr fontFace; FLOAT fontScale; + ::Microsoft::WRL::ComPtr drawingEffect; inline bool ContainsTextPosition(UINT32 desiredTextPosition) const noexcept { @@ -125,11 +131,15 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT STDMETHODCALLTYPE _AnalyzeFontFallback(IDWriteTextAnalysisSource* const source, UINT32 textPosition, UINT32 textLength); [[nodiscard]] HRESULT STDMETHODCALLTYPE _SetMappedFont(UINT32 textPosition, UINT32 textLength, IDWriteFont* const font, FLOAT const scale); + [[nodiscard]] HRESULT STDMETHODCALLTYPE _AnalyzeBoxDrawing(gsl::not_null const source, UINT32 textPosition, UINT32 textLength); + [[nodiscard]] HRESULT STDMETHODCALLTYPE _SetBoxEffect(UINT32 textPosition, UINT32 textLength); + [[nodiscard]] HRESULT _AnalyzeRuns() noexcept; [[nodiscard]] HRESULT _ShapeGlyphRuns() noexcept; [[nodiscard]] HRESULT _ShapeGlyphRun(const UINT32 runIndex, UINT32& glyphStart) noexcept; [[nodiscard]] HRESULT _CorrectGlyphRuns() noexcept; [[nodiscard]] HRESULT _CorrectGlyphRun(const UINT32 runIndex) noexcept; + [[nodiscard]] HRESULT _CorrectBoxDrawing() noexcept; [[nodiscard]] HRESULT _DrawGlyphRuns(_In_opt_ void* clientDrawingContext, IDWriteTextRenderer* renderer, const D2D_POINT_2F origin) noexcept; @@ -148,6 +158,9 @@ namespace Microsoft::Console::Render // DirectWrite font face const ::Microsoft::WRL::ComPtr _font; + // 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/CustomTextRenderer.cpp b/src/renderer/dx/CustomTextRenderer.cpp index 2f4fa5593..b51b9561c 100644 --- a/src/renderer/dx/CustomTextRenderer.cpp +++ b/src/renderer/dx/CustomTextRenderer.cpp @@ -245,7 +245,7 @@ using namespace Microsoft::Console::Render; DWRITE_MEASURING_MODE measuringMode, const DWRITE_GLYPH_RUN* glyphRun, const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription, - IUnknown* /*clientDrawingEffect*/) + IUnknown* clientDrawingEffect) { // Color glyph rendering sourced from https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/DWriteColorGlyph @@ -373,7 +373,8 @@ using namespace Microsoft::Console::Render; measuringMode, glyphRun, glyphRunDescription, - drawingContext->foregroundBrush)); + drawingContext->foregroundBrush, + clientDrawingEffect)); } else { @@ -458,7 +459,8 @@ using namespace Microsoft::Console::Render; measuringMode, &colorRun->glyphRun, colorRun->glyphRunDescription, - layerBrush)); + layerBrush, + clientDrawingEffect)); } break; } @@ -474,7 +476,8 @@ using namespace Microsoft::Console::Render; measuringMode, glyphRun, glyphRunDescription, - drawingContext->foregroundBrush)); + drawingContext->foregroundBrush, + clientDrawingEffect)); } return S_OK; } @@ -485,7 +488,8 @@ using namespace Microsoft::Console::Render; DWRITE_MEASURING_MODE measuringMode, _In_ const DWRITE_GLYPH_RUN* glyphRun, _In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription, - ID2D1Brush* brush) + ID2D1Brush* brush, + _In_opt_ IUnknown* clientDrawingEffect) { RETURN_HR_IF_NULL(E_INVALIDARG, clientDrawingContext); RETURN_HR_IF_NULL(E_INVALIDARG, glyphRun); @@ -494,28 +498,39 @@ using namespace Microsoft::Console::Render; ::Microsoft::WRL::ComPtr d2dContext; RETURN_IF_FAILED(clientDrawingContext->renderTarget->QueryInterface(d2dContext.GetAddressOf())); + // If a special drawing effect was specified, see if we know how to deal with it. + if (clientDrawingEffect) + { + ::Microsoft::WRL::ComPtr boxEffect; + if (SUCCEEDED(clientDrawingEffect->QueryInterface(&boxEffect))) + { + return _DrawBoxRunManually(clientDrawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription, boxEffect.Get()); + } + + //_DrawBasicGlyphRunManually(clientDrawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription); + //_DrawGlowGlyphRun(clientDrawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription); + } + + // If we get down here, there either was no special effect or we don't know what to do with it. Use the standard GlyphRun drawing. + // Using the context is the easiest/default way of drawing. d2dContext->DrawGlyphRun(baselineOrigin, glyphRun, glyphRunDescription, brush, measuringMode); - // However, we could probably add options here and switch out to one of these other drawing methods (making it - // conditional based on the IUnknown* clientDrawingEffect or on some other switches and try these out instead: - - //_DrawBasicGlyphRunManually(clientDrawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription); - //_DrawGlowGlyphRun(clientDrawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription); - return S_OK; } -[[nodiscard]] HRESULT CustomTextRenderer::_DrawBasicGlyphRunManually(DrawingContext* clientDrawingContext, - D2D1_POINT_2F baselineOrigin, - DWRITE_MEASURING_MODE /*measuringMode*/, - _In_ const DWRITE_GLYPH_RUN* glyphRun, - _In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* /*glyphRunDescription*/) noexcept +[[nodiscard]] HRESULT CustomTextRenderer::_DrawBoxRunManually(DrawingContext* clientDrawingContext, + D2D1_POINT_2F baselineOrigin, + DWRITE_MEASURING_MODE /*measuringMode*/, + _In_ const DWRITE_GLYPH_RUN* glyphRun, + _In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* /*glyphRunDescription*/, + _In_ IBoxDrawingEffect* clientDrawingEffect) noexcept +try { RETURN_HR_IF_NULL(E_INVALIDARG, clientDrawingContext); RETURN_HR_IF_NULL(E_INVALIDARG, glyphRun); + RETURN_HR_IF_NULL(E_INVALIDARG, clientDrawingEffect); - // This is regular text but manually ::Microsoft::WRL::ComPtr d2dFactory; clientDrawingContext->renderTarget->GetFactory(d2dFactory.GetAddressOf()); @@ -537,17 +552,90 @@ using namespace Microsoft::Console::Render; geometrySink->Close(); - D2D1::Matrix3x2F const matrixAlign = D2D1::Matrix3x2F::Translation(baselineOrigin.x, baselineOrigin.y); + // Can be used to see the dimensions of what is written. + /*D2D1_RECT_F bounds; + pathGeometry->GetBounds(D2D1::IdentityMatrix(), &bounds);*/ + // The bounds here are going to be centered around the baseline of the font. + // That is, the DWRITE_GLYPH_METRICS property for this glyph's baseline is going + // to be at the 0 point in the Y direction when we receive the geometry. + // The ascent will go up negative from Y=0 and the descent will go down positive from Y=0. + // As for the horizontal direction, I didn't study this in depth, but it appears to always be + // positive X with both the left and right edges being positive and away from X=0. + // For one particular instance, we might ask for the geometry for a U+2588 box and see the bounds as: + // + // Top= + // -20.315 + // ----------- + // | | + // | | + // Left= | | Right= + // 13.859 | | 26.135 + // | | + // Origin --> X | | + // (0,0) | | + // ----------- + // Bottom= + // 5.955 + + // Dig out the box drawing effect parameters. + BoxScale scale; + RETURN_IF_FAILED(clientDrawingEffect->GetScale(&scale)); + + // The scale transform will inflate the entire geometry first. + // We want to do this before it moves out of its original location as generally our + // algorithms for fitting cells will blow up the glyph to the size it needs to be first and then + // nudge it into place with the translations. + const auto scaleTransform = D2D1::Matrix3x2F::Scale(scale.HorizontalScale, scale.VerticalScale); + + // Now shift it all the way to where the baseline says it should be. + const auto baselineTransform = D2D1::Matrix3x2F::Translation(baselineOrigin.x, baselineOrigin.y); + + // Finally apply the little "nudge" that we may have been directed to align it better with the cell. + const auto offsetTransform = D2D1::Matrix3x2F::Translation(scale.HorizontalTranslation, scale.VerticalTranslation); + + // The order is important here. Scale it first, then slide it into place. + const auto matrixTransformation = scaleTransform * baselineTransform * offsetTransform; ::Microsoft::WRL::ComPtr transformedGeometry; d2dFactory->CreateTransformedGeometry(pathGeometry.Get(), - &matrixAlign, + &matrixTransformation, transformedGeometry.GetAddressOf()); + // Can be used to see the dimensions after translation. + /*D2D1_RECT_F boundsAfter; + transformedGeometry->GetBounds(D2D1::IdentityMatrix(), &boundsAfter);*/ + // Compare this to the original bounds above to see what the matrix did. + // To make it useful, first visualize for yourself the pixel dimensions of the cell + // based on the baselineOrigin and the exact integer cell width and heights that we're storing. + // You'll also probably need the full-pixel ascent and descent because the point we're given + // is the baseline, not the top left corner of the cell as we're used to. + // Most of these metrics can be found in the initial font creation routines or in + // the line spacing applied to the text format (member variables on the renderer). + // baselineOrigin = (0, 567) + // fullPixelAscent = 39 + // fullPixelDescent = 9 + // cell dimensions = 26 x 48 (notice 48 height is 39 + 9 or ascent + descent) + // This means that our cell should be the rectangle + // + // T=528 + // |-------| + // L=0 | | + // | | + // Baseline->x | + // Origin | | R=26 + // |-------| + // B=576 + // + // And we'll want to check that the bounds after transform will fit the glyph nicely inside + // this box. + // If not? We didn't do the scaling or translation correctly. Oops. + + // Fill in the geometry. Don't outline, it can leave stuff outside the area we expect. clientDrawingContext->renderTarget->FillGeometry(transformedGeometry.Get(), clientDrawingContext->foregroundBrush); return S_OK; } +CATCH_RETURN(); [[nodiscard]] HRESULT CustomTextRenderer::_DrawGlowGlyphRun(DrawingContext* clientDrawingContext, D2D1_POINT_2F baselineOrigin, diff --git a/src/renderer/dx/CustomTextRenderer.h b/src/renderer/dx/CustomTextRenderer.h index de6e359d1..3ba355cc1 100644 --- a/src/renderer/dx/CustomTextRenderer.h +++ b/src/renderer/dx/CustomTextRenderer.h @@ -4,6 +4,7 @@ #pragma once #include +#include "BoxDrawingEffect.h" namespace Microsoft::Console::Render { @@ -98,13 +99,15 @@ namespace Microsoft::Console::Render DWRITE_MEASURING_MODE measuringMode, _In_ const DWRITE_GLYPH_RUN* glyphRun, _In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription, - ID2D1Brush* brush); + ID2D1Brush* brush, + _In_opt_ IUnknown* clientDrawingEffect); - [[nodiscard]] HRESULT _DrawBasicGlyphRunManually(DrawingContext* clientDrawingContext, - D2D1_POINT_2F baselineOrigin, - DWRITE_MEASURING_MODE measuringMode, - _In_ const DWRITE_GLYPH_RUN* glyphRun, - _In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription) noexcept; + [[nodiscard]] HRESULT _DrawBoxRunManually(DrawingContext* clientDrawingContext, + D2D1_POINT_2F baselineOrigin, + DWRITE_MEASURING_MODE measuringMode, + _In_ const DWRITE_GLYPH_RUN* glyphRun, + _In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription, + _In_ IBoxDrawingEffect* clientDrawingEffect) noexcept; [[nodiscard]] HRESULT _DrawGlowGlyphRun(DrawingContext* clientDrawingContext, D2D1_POINT_2F baselineOrigin, diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index de6174e6b..9a255f976 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -81,6 +81,7 @@ DxEngine::DxEngine() : _backgroundColor{ 0 }, _selectionBackground{}, _glyphCell{}, + _boxDrawingEffect{}, _haveDeviceResources{ false }, _retroTerminalEffects{ false }, _antialiasingMode{ D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE }, @@ -1230,7 +1231,8 @@ try _dwriteTextFormat.Get(), _dwriteFontFace.Get(), clusters, - _glyphCell.width()); + _glyphCell.width(), + _boxDrawingEffect.Get()); // Get the baseline for this font as that's where we draw from DWRITE_LINE_SPACING spacing; @@ -1606,6 +1608,9 @@ try _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 S_OK; } CATCH_RETURN(); @@ -1739,7 +1744,8 @@ try _dwriteTextFormat.Get(), _dwriteFontFace.Get(), { &cluster, 1 }, - _glyphCell.width()); + _glyphCell.width(), + _boxDrawingEffect.Get()); UINT32 columns = 0; RETURN_IF_FAILED(layout.GetColumns(&columns)); @@ -1990,6 +1996,9 @@ CATCH_RETURN(); 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 @@ -2029,6 +2038,10 @@ CATCH_RETURN(); 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; @@ -2041,19 +2054,41 @@ CATCH_RETURN(); // and set the baseline to the full round pixel ascent value. // // For reference, for the letters "ag": - // aaaaaa ggggggg <=================================== - // a g g | | - // aaaaa ggggg |<-ascent | - // a a g | |---- height - // aaaaa a gggggg <-------------------baseline | - // g g |<-descent | - // gggggg <=================================== + // ... + // gggggg bottom of previous line // - const auto fullPixelAscent = ceil(ascent); - const auto fullPixelDescent = ceil(descent); + // ----------------- <===========================================| + // | 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 @@ -2084,7 +2119,7 @@ CATCH_RETURN(); // of hit testing math and other such multiplication/division. COORD coordSize = { 0 }; coordSize.X = gsl::narrow(widthExact); - coordSize.Y = gsl::narrow(lineSpacing.height); + 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 diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index ec19ee9d7..22f73c3e2 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -134,6 +134,7 @@ namespace Microsoft::Console::Render til::size _displaySizePixels; til::size _glyphCell; + ::Microsoft::WRL::ComPtr _boxDrawingEffect; D2D1_COLOR_F _defaultForegroundColor; D2D1_COLOR_F _defaultBackgroundColor; diff --git a/src/renderer/dx/IBoxDrawingEffect.idl b/src/renderer/dx/IBoxDrawingEffect.idl new file mode 100644 index 000000000..14443a054 --- /dev/null +++ b/src/renderer/dx/IBoxDrawingEffect.idl @@ -0,0 +1,20 @@ +import "oaidl.idl"; +import "ocidl.idl"; + +typedef struct BoxScale +{ + float VerticalScale; + float VerticalTranslation; + float HorizontalScale; + float HorizontalTranslation; +} BoxScale; + +[ + uuid("C164926F-1A4D-470D-BB8A-3D2CC4B035E4"), + object, + local +] +interface IBoxDrawingEffect : IUnknown +{ + HRESULT GetScale([out] BoxScale* scale); +}; diff --git a/src/renderer/dx/lib/dx.vcxproj b/src/renderer/dx/lib/dx.vcxproj index f36c75f99..8eb6ba6e9 100644 --- a/src/renderer/dx/lib/dx.vcxproj +++ b/src/renderer/dx/lib/dx.vcxproj @@ -6,10 +6,16 @@ dx RendererDx ConRenderDx - StaticLibrary + StaticLibrary + + + $(SolutionDir)src\renderer\dx\ + + + @@ -18,6 +24,7 @@ + @@ -25,6 +32,9 @@ + + + diff --git a/src/renderer/dx/lib/dx.vcxproj.filters b/src/renderer/dx/lib/dx.vcxproj.filters index bbfb86894..4a0cc0df7 100644 --- a/src/renderer/dx/lib/dx.vcxproj.filters +++ b/src/renderer/dx/lib/dx.vcxproj.filters @@ -8,6 +8,7 @@ + @@ -16,5 +17,9 @@ + + + + \ No newline at end of file