terminal/src/renderer/dx/CustomTextLayout.h
Michael Niksa 7f43b40da9
Improve glyph scaling correction (#4747)
## Summary of the Pull Request
- Improves the correction of the scaling and spacing that is applied to
  glyphs if they are too large or too small for the number of columns that
  the text buffer is expecting

## References
- Supersedes #4438 
Co-authored-by: Mili (Yi) Zhang <milizhang@gmail.com>

- Related to #4704 (#4731)

## PR Checklist
* [x] Closes #696 
* [x] Closes #4375 
* [x] Closes #4708 
* [x] Closes a crash that @DHowett-MSFT complained about with
  `"x" * ($Host.UI.RawUI.BufferSize.Width - 1) + "`u{241b}"`
* [x] Eliminates an exception getting thrown with the U+1F3E0 emoji in
  `_CorrectGlyphRun`
* [x] Corrects several graphical issues that occurred after #4731 was
  merged to master (weird repeats and splits of runs)
* [x] I work here.
* [x] Tested manually versus given scenarios.
* [x] Documentation written into comments in the code.
* [x] I'm a core contributor.

## Detailed Description of the Pull Request / Additional comments
- The `_CorrectGlyphRun` function now walks through and uses the
  `_glyphClusters` map to determine the text span and glyph span for each
  cluster so it can be considered as a single unit for scaling.
- The total number of columns expected across the entire cluster
  text/glyph unit is considered for the available spacing for drawing
- The total glyph advances are summed to see how much space they will
  take
- If more space than necessary to draw, all glyphs in the cluster are
  offset into the center and the extra space is padded onto the advance of
  the last glyph in the range.
- If less space than necessary to draw, the entire cluster is marked for
  shrinking as a single unit by providing the initial text index and
  length (that is back-mapped out of the glyph run) up to the parent
  function so it can use the `_SetCurrentRun` and `_SplitCurrentRun`
  existing functions (which operate on text) to split the run into pieces
  and only scale the one glyph cluster, not things next to it as well.
- The scale factor chosen for shrinking is now based on the proportion
  of the advances instead of going through some font math wizardry
- The parent that calls the run splitting functions now checks to not
  attempt to split off text after the cluster if it's already at the end.
  This was @DHowett-MSFT's crash.
- The split run function has been corrected to fix the `glyphStart`
  position of the back half (it failed to `+=` instead of `=` which
  resulted in duplicated text, sometimes).
- Surrogate pair emoji were not allocating an appropriate number of
  `_textClusterColumns`. The constructor has been updated such that the
  trailing half of surrogate pairs gets a 0 column width (as the lead is
  marked appropriately by the `GetColumns()` function). This was the
  exception thrown.
- The `_glyphScaleCorrections` array stored up over the calls to
  `_CorrectGlyphRun` now uses a struct `ScaleCorrection` as we're up to 3
  values.
- The `ScaleCorrection` values are named to clearly indicate they're in
  relation to the original text span, not the glyph spans.
- The values that are used to construct `ScaleCorrection`s within
  `_CorrectGlyphRun` have been double checked and corrected to not
  accidentally use glyph index/counts when text index/counts are what's
  required.

## Validation Steps Performed
- Tested the utf82.txt file from one of the linked bugs. Looked
  specifically at Burmese through Thai to ensure restoration (for the most
  part) of the behavior
- Ensured that U+1f3e0 emoji (🏠) continues to draw correctly
- Checked Fixedsys Excelsior font to ensure it's not shrinking the line
  with its ligatures
- Checked ligatureness of Cascadia Code font 
- Checked combining characters U+0300-U+0304 with a capital A
2020-03-02 19:21:07 +00:00

201 lines
9.5 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <dwrite.h>
#include <d2d1.h>
#include <wrl.h>
#include <wrl/client.h>
#include <wrl/implements.h>
#include "../inc/Cluster.hpp"
namespace Microsoft::Console::Render
{
class CustomTextLayout : public ::Microsoft::WRL::RuntimeClass<::Microsoft::WRL::RuntimeClassFlags<::Microsoft::WRL::ClassicCom | ::Microsoft::WRL::InhibitFtmBase>, IDWriteTextAnalysisSource, IDWriteTextAnalysisSink>
{
public:
// Based on the Windows 7 SDK sample at https://github.com/pauldotknopf/WindowsSDK7-Samples/tree/master/multimedia/DirectWrite/CustomLayout
CustomTextLayout(gsl::not_null<IDWriteFactory1*> const factory,
gsl::not_null<IDWriteTextAnalyzer1*> const analyzer,
gsl::not_null<IDWriteTextFormat*> const format,
gsl::not_null<IDWriteFontFace1*> const font,
const std::basic_string_view<::Microsoft::Console::Render::Cluster> clusters,
size_t const width);
[[nodiscard]] HRESULT STDMETHODCALLTYPE GetColumns(_Out_ UINT32* columns);
// IDWriteTextLayout methods (but we don't actually want to implement them all, so just this one matching the existing interface)
[[nodiscard]] HRESULT STDMETHODCALLTYPE Draw(_In_opt_ void* clientDrawingContext,
_In_ IDWriteTextRenderer* renderer,
FLOAT originX,
FLOAT originY) noexcept;
// IDWriteTextAnalysisSource methods
[[nodiscard]] HRESULT STDMETHODCALLTYPE GetTextAtPosition(UINT32 textPosition,
_Outptr_result_buffer_(*textLength) WCHAR const** textString,
_Out_ UINT32* textLength) override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE GetTextBeforePosition(UINT32 textPosition,
_Outptr_result_buffer_(*textLength) WCHAR const** textString,
_Out_ UINT32* textLength) noexcept override;
[[nodiscard]] DWRITE_READING_DIRECTION STDMETHODCALLTYPE GetParagraphReadingDirection() noexcept override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE GetLocaleName(UINT32 textPosition,
_Out_ UINT32* textLength,
_Outptr_result_z_ WCHAR const** localeName) noexcept override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE GetNumberSubstitution(UINT32 textPosition,
_Out_ UINT32* textLength,
_COM_Outptr_ IDWriteNumberSubstitution** numberSubstitution) noexcept override;
// IDWriteTextAnalysisSink methods
[[nodiscard]] HRESULT STDMETHODCALLTYPE SetScriptAnalysis(UINT32 textPosition,
UINT32 textLength,
_In_ DWRITE_SCRIPT_ANALYSIS const* scriptAnalysis) override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE SetLineBreakpoints(UINT32 textPosition,
UINT32 textLength,
_In_reads_(textLength) DWRITE_LINE_BREAKPOINT const* lineBreakpoints) override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE SetBidiLevel(UINT32 textPosition,
UINT32 textLength,
UINT8 explicitLevel,
UINT8 resolvedLevel) override;
[[nodiscard]] HRESULT STDMETHODCALLTYPE SetNumberSubstitution(UINT32 textPosition,
UINT32 textLength,
_In_ IDWriteNumberSubstitution* numberSubstitution) override;
protected:
// A single contiguous run of characters containing the same analysis results.
struct Run
{
Run() noexcept :
textStart(),
textLength(),
glyphStart(),
glyphCount(),
bidiLevel(),
script(),
isNumberSubstituted(),
isSideways(),
fontFace{ nullptr },
fontScale{ 1.0 }
{
}
UINT32 textStart; // starting text position of this run
UINT32 textLength; // number of contiguous code units covered
UINT32 glyphStart; // starting glyph in the glyphs array
UINT32 glyphCount; // number of glyphs associated with this run of text
DWRITE_SCRIPT_ANALYSIS script;
UINT8 bidiLevel;
bool isNumberSubstituted;
bool isSideways;
::Microsoft::WRL::ComPtr<IDWriteFontFace1> fontFace;
FLOAT fontScale;
inline bool ContainsTextPosition(UINT32 desiredTextPosition) const noexcept
{
return desiredTextPosition >= textStart && desiredTextPosition < textStart + textLength;
}
inline bool operator==(UINT32 desiredTextPosition) const noexcept
{
// Search by text position using std::find
return ContainsTextPosition(desiredTextPosition);
}
};
// Single text analysis run, which points to the next run.
struct LinkedRun : Run
{
LinkedRun() noexcept :
nextRunIndex(0)
{
}
UINT32 nextRunIndex; // index of next run
};
[[nodiscard]] LinkedRun& _FetchNextRun(UINT32& textLength);
[[nodiscard]] LinkedRun& _GetCurrentRun();
void _SetCurrentRun(const UINT32 textPosition);
void _SplitCurrentRun(const UINT32 splitPosition);
void _OrderRuns();
[[nodiscard]] HRESULT STDMETHODCALLTYPE _AnalyzeFontFallback(IDWriteTextAnalysisSource* const source, UINT32 textPosition, UINT32 textLength);
[[nodiscard]] HRESULT STDMETHODCALLTYPE _SetMappedFont(UINT32 textPosition, UINT32 textLength, IDWriteFont* const font, FLOAT const scale);
[[nodiscard]] HRESULT _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 _DrawGlyphRuns(_In_opt_ void* clientDrawingContext,
IDWriteTextRenderer* renderer,
const D2D_POINT_2F origin) noexcept;
[[nodiscard]] static constexpr UINT32 _EstimateGlyphCount(const UINT32 textLength) noexcept;
private:
const ::Microsoft::WRL::ComPtr<IDWriteFactory1> _factory;
// DirectWrite analyzer
const ::Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1> _analyzer;
// DirectWrite text format
const ::Microsoft::WRL::ComPtr<IDWriteTextFormat> _format;
// DirectWrite font face
const ::Microsoft::WRL::ComPtr<IDWriteFontFace1> _font;
// The text we're analyzing and processing into a layout
std::wstring _text;
std::vector<UINT16> _textClusterColumns;
size_t _width;
// Properties of the text that might be relevant.
std::wstring _localeName;
::Microsoft::WRL::ComPtr<IDWriteNumberSubstitution> _numberSubstitution;
DWRITE_READING_DIRECTION _readingDirection;
// Text analysis results
std::vector<LinkedRun> _runs;
std::vector<DWRITE_LINE_BREAKPOINT> _breakpoints;
// Text analysis interim status variable (to assist the Analyzer Sink in operations involving _runs)
UINT32 _runIndex;
// Glyph shaping results
std::vector<DWRITE_GLYPH_OFFSET> _glyphOffsets;
// Clusters are complicated. They're in respect to each individual run.
// The offsets listed here are in respect to the _text string, but from the beginning index of
// each run.
// That means if we have two runs, we will see 0 1 2 3 4 0 1 2 3 4 5 6 7... in this clusters count.
std::vector<UINT16> _glyphClusters;
// This appears to be the index of the glyph inside each font.
std::vector<UINT16> _glyphIndices;
std::vector<float> _glyphAdvances;
struct ScaleCorrection
{
UINT32 textIndex;
UINT32 textLength;
float scale;
};
// These are used to further break the runs apart and adjust the font size so glyphs fit inside the cells.
std::vector<ScaleCorrection> _glyphScaleCorrections;
#ifdef UNIT_TESTING
public:
CustomTextLayout() = default;
friend class CustomTextLayoutTests;
#endif
};
}