Skip to content

Avoid reflow of shaped text #3566

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 1 commit into from
Oct 7, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1504,7 +1504,7 @@ protected sealed override Size ArrangeOverride(Size arrangeSize)
{
// Check if paragraph ellipsis are added to this line
bool ellipsis = ParagraphEllipsisShownOnLine(i, lineOffset.Y - contentOffset.Y);
line.Format(dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), lineMetrics.TextLineBreak, _textBlockCache._textRunCache, ellipsis);
Format(line, lineMetrics.Length, dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), lineMetrics.TextLineBreak, _textBlockCache._textRunCache, ellipsis);

// Check that lineMetrics length and line length are in sync
// Workaround for (Crash when mouse over a Button with TextBlock). Re-enable this assert when MIL Text issue is fixed.
Expand Down Expand Up @@ -1638,7 +1638,7 @@ protected sealed override void OnRender(DrawingContext ctx)
{
using (line)
{
line.Format(dcp, wrappingWidth, GetLineProperties(dcp == 0, showParagraphEllipsis, lineProperties), lineMetrics.TextLineBreak, _textBlockCache._textRunCache, showParagraphEllipsis);
Format(line, lineMetrics.Length, dcp, wrappingWidth, GetLineProperties(dcp == 0, showParagraphEllipsis, lineProperties), lineMetrics.TextLineBreak, _textBlockCache._textRunCache, showParagraphEllipsis);

// Workaround for (Crash when mouse over a Button with TextBlock). Re-enable this assert when MIL Text issue is fixed.
//if (!showParagraphEllipsis)
Expand Down Expand Up @@ -1778,7 +1778,7 @@ protected virtual IInputElement InputHitTestCore(Point point)
{
// Check if paragraph ellipsis are rendered
bool ellipsis = ParagraphEllipsisShownOnLine(i, lineOffset);
line.Format(dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, ellipsis);
Format(line, lineMetrics.Length, dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, ellipsis);

// Verify consistency of line formatting
// Check that lineMetrics.Length is in sync with line.Length
Expand Down Expand Up @@ -1902,7 +1902,7 @@ protected virtual ReadOnlyCollection<Rect> GetRectanglesCore(ContentElement chil
{
// Check if paragraph ellipsis are rendered
bool ellipsis = ParagraphEllipsisShownOnLine(lineIndex, lineOffset);
line.Format(lineStart, wrappingWidth, GetLineProperties(lineIndex == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, ellipsis);
Format(line, lineMetrics.Length, lineStart, wrappingWidth, GetLineProperties(lineIndex == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, ellipsis);

// Verify consistency of line formatting
// Workaround for (Crash when mouse over a Button with TextBlock). Re-enable this assert when MIL Text issue is fixed.
Expand Down Expand Up @@ -2171,16 +2171,17 @@ internal void GetLineDetails(int dcp, int index, double lineVOffset, out int cch
double wrappingWidth = CalcWrappingWidth(RenderSize.Width);

TextRunCache textRunCache = new TextRunCache();
LineMetrics lineMetrics = GetLine(index);

// Retrieve details from the line.
using(Line line = CreateLine(lineProperties))
using (Line line = CreateLine(lineProperties))
{
// Format line. Set showParagraphEllipsis flag to false
TextLineBreak textLineBreak = GetLine(index).TextLineBreak;
bool ellipsis = ParagraphEllipsisShownOnLine(index, lineVOffset);
line.Format(dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), textLineBreak, textRunCache, ellipsis);
Format(line, lineMetrics.Length, dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), textLineBreak, textRunCache, ellipsis);

MS.Internal.Invariant.Assert(GetLine(index).Length == line.Length, "Line length is out of sync");
MS.Internal.Invariant.Assert(lineMetrics.Length == line.Length, "Line length is out of sync");

cchContent = line.ContentLength;
cchEllipses = line.GetEllipsesLength();
Expand Down Expand Up @@ -2219,15 +2220,16 @@ internal ITextPointer GetTextPositionFromDistance(int dcp, double distance, doub
lineVOffset -= contentOffset.Y;

TextRunCache textRunCache = new TextRunCache();
LineMetrics lineMetrics = GetLine(index);
ITextPointer pos;
using(Line line = CreateLine(lineProperties))
{
MS.Internal.Invariant.Assert(index >= 0 && index < LineCount);
TextLineBreak textLineBreak = GetLine(index).TextLineBreak;
bool ellipsis = ParagraphEllipsisShownOnLine(index, lineVOffset);
line.Format(dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), textLineBreak, textRunCache, ellipsis);
Format(line, lineMetrics.Length, dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), textLineBreak, textRunCache, ellipsis);

MS.Internal.Invariant.Assert(GetLine(index).Length == line.Length, "Line length is out of sync");
MS.Internal.Invariant.Assert(lineMetrics.Length == line.Length, "Line length is out of sync");

CharacterHit charIndex = line.GetTextPositionFromDistance(distance);
LogicalDirection logicalDirection;
Expand Down Expand Up @@ -2297,7 +2299,7 @@ internal Rect GetRectangleFromTextPosition(ITextPointer orientedPosition)
using(Line line = CreateLine(lineProperties))
{
bool ellipsis = ParagraphEllipsisShownOnLine(i, lineOffset);
line.Format(dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, ellipsis);
Format(line, lineMetrics.Length, dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, ellipsis);

// Check consistency of line length
MS.Internal.Invariant.Assert(lineMetrics.Length == line.Length, "Line length is out of sync");
Expand Down Expand Up @@ -2399,7 +2401,7 @@ internal Geometry GetTightBoundingGeometryFromTextPositions(ITextPointer startPo
using (line)
{
bool ellipsis = ParagraphEllipsisShownOnLine(i, lineOffset);
line.Format(dcpLineStart, wrappingWidth, GetLineProperties(dcpLineStart == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, ellipsis);
Format(line, lineMetrics.Length, dcpLineStart, wrappingWidth, GetLineProperties(dcpLineStart == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, ellipsis);

if (Invariant.Strict)
{
Expand Down Expand Up @@ -2503,7 +2505,7 @@ internal bool IsAtCaretUnitBoundary(ITextPointer position, int dcp, int lineInde
{
// Format line. Set showParagraphEllipsis flag to false since we are not using information about
// ellipsis to change line offsets in this case.
line.Format(dcp, wrappingWidth, GetLineProperties(lineIndex == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, false);
Format(line, lineMetrics.Length, dcp, wrappingWidth, GetLineProperties(lineIndex == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, false);

// Check consistency of line formatting
MS.Internal.Invariant.Assert(lineMetrics.Length == line.Length, "Line length is out of sync");
Expand Down Expand Up @@ -2591,7 +2593,7 @@ internal ITextPointer GetNextCaretUnitPosition(ITextPointer position, LogicalDir
{
// Format line. Set showParagraphEllipsis flag to false since we are not using information about
// ellipsis to change line offsets in this case.
line.Format(dcp, wrappingWidth, GetLineProperties(lineIndex == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, false);
Format(line, lineMetrics.Length, dcp, wrappingWidth, GetLineProperties(lineIndex == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, false);

// Check consistency of line formatting
MS.Internal.Invariant.Assert(lineMetrics.Length == line.Length, "Line length is out of sync");
Expand Down Expand Up @@ -2700,7 +2702,7 @@ internal ITextPointer GetBackspaceCaretUnitPosition(ITextPointer position, int d
{
// Format line. Set showParagraphEllipsis flag to false since we are not using information about
// ellipsis to change line offsets in this case.
line.Format(dcp, wrappingWidth, GetLineProperties(lineIndex == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, false);
Format(line, lineMetrics.Length, dcp, wrappingWidth, GetLineProperties(lineIndex == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, false);

// Check consistency of line formatting
MS.Internal.Invariant.Assert(lineMetrics.Length == line.Length, "Line length is out of sync");
Expand Down Expand Up @@ -3326,6 +3328,108 @@ private double CalcWrappingWidth(double width)
return width;
}

// ------------------------------------------------------------------
// Wrapper for line.Format that tries to make the same line-break decisions as Measure
// ------------------------------------------------------------------
private void Format(Line line, int length, int dcp, double wrappingWidth, TextParagraphProperties paragraphProperties, TextLineBreak textLineBreak, TextRunCache textRunCache, bool ellipsis)
{
line.Format(dcp, wrappingWidth, paragraphProperties, textLineBreak, textRunCache, ellipsis);

// line.Format can reflow (make a different line-break
// decision than it did during measure), contrary to the comment
// in CalcWrappingWidth "Reflowing will not happen when Width is
// between _previousDesiredSize.Width and ReferenceWidth", if the
// line contains text that gets shaped in a way that reduces the
// total width. Here is an example.
// Text="ABCDE IAATA Corp." TextWrapping=Wrap ReferenceWidth=115
// 1. Measure calls FormatLine(115), which determines that the full
// text is wider than 115 and breaks it after the second word.
// The resulting desired width is 83.3167 - the length of
// the first line "ABCDE IAATA"
// 2. Render, HitTest, et al. call FormatLine(83.3167), which determines
// that the first two words are already wider than 83.3167 and
// breaks after the first word.
// 3. FormatLine uses unshaped glyph widths to determine how much text
// to consider in line-breaking decisions. But it reports the
// width of the lines it produces using shaped glyph widths.
// In the example, the sequence "ATA" gets kerned closer together,
// making the shaped width of the first two words (83.3167)
// about 2.6 pixels less than the unshaped width (85.96).
// This is enough to produce the "reflowing".
// The consequences of reflowing are bad. In the example, the second
// word is not rendered, and programmatic editing crashes with FailFast.
//
// In light of this, we need to work harder to ensure that reflowing
// doesn't happen. The obvious idea to accomplish this is to change
// FormatLine to use shaped widths throughout, but that would mean
// changing the callbacks from LineServices and DWrite, and asserting
// that the changes have no unforseen consequences - out of scope.
// Instead, we'll call FormatLine with a target width large enough
// to produce the right line-break.
//
// This has consequences, especially when TextAlignment=Justify -
// the line is justified to the larger width rather than to wrappingWidth,
// which makes the text extend past the arrange-rect. To mitigate this,
// use the smallest width between wrappingWidth and ReferenceWidth that produces the
// right line-break.
//
// This fixes the cases of missing text and FailFast, at the cost of
// 1. more calls to FormatLine (perf hit)
// 2. justified text sticks out of the arrange-rect
// It's pay-for-play - we only do it on lines that reflow.

if (line.Length < length) // reflow happened
{
double goodWidth = _referenceSize.Width; // no reflow at this width
double badWidth = wrappingWidth; // reflow at this width
// Make sure that TextFormatter limitations are not exceeded.
TextDpi.EnsureValidLineWidth(ref goodWidth); // wrappingWidth is already valid, per CalcWrappingWidth

// The smallest good width can't be calcluated in advance, as it's
// dependent on the shaped and unshaped glyph-widths and the available
// width in a complicated way. Instead, binary search.
const double tolerance = 0.01; // allow a small overshoot, to limit the number of iterations

// In practice, the smallest good width is quite close to wrappingWidth,
// so start with "bottom-up binary search".
for (double delta = tolerance; /* goodWidth not found */; delta *= 2.0)
{
double width = badWidth + delta;
if (width > goodWidth)
break; // don't increase goodWidth

line.Format(dcp, width, paragraphProperties, textLineBreak, textRunCache, ellipsis);
if (line.Length < length)
{
badWidth = width;
}
else
{
goodWidth = width;
break;
}
}

// now do a regular binary search on the remaining interval
for (double delta = (goodWidth - badWidth) / 2.0; delta > tolerance; delta /= 2.0)
{
double width = badWidth + delta;
line.Format(dcp, width, paragraphProperties, textLineBreak, textRunCache, ellipsis);
if (line.Length < length)
{
badWidth = width;
}
else
{
goodWidth = width;
}
}

// now format at goodwidth, with no reflow
line.Format(dcp, goodWidth, paragraphProperties, textLineBreak, textRunCache, ellipsis);
}
}

// ------------------------------------------------------------------
// Aborts calculation by throwing exception if world has changed
// while in measure / arrange / render process.
Expand Down Expand Up @@ -3415,7 +3519,7 @@ private void AlignContent()
using (line)
{
bool ellipsis = ParagraphEllipsisShownOnLine(i, lineOffset);
line.Format(dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, ellipsis);
Format(line, lineMetrics.Length, dcp, wrappingWidth, GetLineProperties(dcp == 0, lineProperties), lineMetrics.TextLineBreak, textRunCache, ellipsis);
double lineHeight = CalcLineAdvance(line.Height, lineProperties);

// Check consistency of line formatting
Expand Down