Browse Source

Merge pull request #8053 from Gillibald/fixes/textProcessingBugs

Fix text processing bugs
pull/8083/head
Benedikt Stebner 4 years ago
committed by GitHub
parent
commit
1554ce6507
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  2. 6
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  3. 91
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  4. 25
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  5. 19
      src/Avalonia.Controls/TextBlock.cs
  6. 7
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  7. 33
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  8. BIN
      tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png
  9. BIN
      tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png

6
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -528,7 +528,7 @@ namespace Avalonia.Media.TextFormatting
/// Creates an empty text line. /// Creates an empty text line.
/// </summary> /// </summary>
/// <returns>The empty text line.</returns> /// <returns>The empty text line.</returns>
public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, TextParagraphProperties paragraphProperties) public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties)
{ {
var flowDirection = paragraphProperties.FlowDirection; var flowDirection = paragraphProperties.FlowDirection;
var properties = paragraphProperties.DefaultTextRunProperties; var properties = paragraphProperties.DefaultTextRunProperties;
@ -542,7 +542,7 @@ namespace Avalonia.Media.TextFormatting
var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) }; var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
return new TextLineImpl(textRuns, firstTextSourceIndex, 0, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine(); return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine();
} }
/// <summary> /// <summary>
@ -561,7 +561,7 @@ namespace Avalonia.Media.TextFormatting
{ {
if(textRuns.Count == 0) if(textRuns.Count == 0)
{ {
return CreateEmptyTextLine(firstTextSourceIndex, paragraphProperties); return CreateEmptyTextLine(firstTextSourceIndex,paragraphWidth, paragraphProperties);
} }
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))

6
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -410,7 +410,7 @@ namespace Avalonia.Media.TextFormatting
{ {
if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{ {
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, _paragraphProperties); var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
Bounds = new Rect(0,0,0, textLine.Height); Bounds = new Rect(0,0,0, textLine.Height);
@ -434,7 +434,7 @@ namespace Avalonia.Media.TextFormatting
{ {
if(previousLine != null && previousLine.NewLineLength > 0) if(previousLine != null && previousLine.NewLineLength > 0)
{ {
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, _paragraphProperties); var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties);
textLines.Add(emptyTextLine); textLines.Add(emptyTextLine);
@ -483,7 +483,7 @@ namespace Avalonia.Media.TextFormatting
//Make sure the TextLayout always contains at least on empty line //Make sure the TextLayout always contains at least on empty line
if(textLines.Count == 0) if(textLines.Count == 0)
{ {
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, _paragraphProperties); var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);
textLines.Add(textLine); textLines.Add(textLine);

91
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -252,7 +252,7 @@ namespace Avalonia.Media.TextFormatting
//Look at the left and right edge of the current run //Look at the left and right edge of the current run
if (currentRun.IsLeftToRight) if (currentRun.IsLeftToRight)
{ {
if (lastRun == null || lastRun.IsLeftToRight) if (_flowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight))
{ {
if (characterIndex <= textRun.Text.Start) if (characterIndex <= textRun.Text.Start)
{ {
@ -418,11 +418,6 @@ namespace Avalonia.Media.TextFormatting
continue; continue;
} }
if(currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex)
{
continue;
}
TextRun? nextRun = null; TextRun? nextRun = null;
if (index + 1 < TextRuns.Count) if (index + 1 < TextRuns.Count)
@ -460,7 +455,7 @@ namespace Avalonia.Media.TextFormatting
} }
default: default:
{ {
goto noop; goto noop;
} }
} }
@ -541,6 +536,11 @@ namespace Avalonia.Media.TextFormatting
endX += currentRun.Size.Width; endX += currentRun.Size.Width;
} }
if (currentPosition < firstTextSourceCharacterIndex)
{
startX += currentRun.Size.Width;
}
currentPosition += currentRun.TextSourceLength; currentPosition += currentRun.TextSourceLength;
break; break;
@ -554,24 +554,29 @@ namespace Avalonia.Media.TextFormatting
var width = endX - startX; var width = endX - startX;
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) if (!MathUtilities.IsZero(width))
{ {
currentRect = currentRect.WithWidth(currentRect.Width + width); if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
{
currentRect = currentRect.WithWidth(currentRect.Width + width);
var textBounds = new TextBounds(currentRect, currentDirection); var textBounds = new TextBounds(currentRect, currentDirection);
result[result.Count - 1] = textBounds; result[result.Count - 1] = textBounds;
} }
else else
{ {
currentRect = new Rect(startX, 0, width, Height);
result.Add(new TextBounds(currentRect, currentDirection)); currentRect = new Rect(startX, 0, width, Height);
result.Add(new TextBounds(currentRect, currentDirection));
}
} }
if (currentDirection == FlowDirection.LeftToRight) if (currentDirection == FlowDirection.LeftToRight)
{ {
if (currentPosition >= firstTextSourceCharacterIndex + textLength) if (currentPosition > firstTextSourceCharacterIndex + textLength)
{ {
break; break;
} }
@ -595,10 +600,10 @@ namespace Avalonia.Media.TextFormatting
public TextLineImpl FinalizeLine() public TextLineImpl FinalizeLine()
{ {
BidiReorder();
_textLineMetrics = CreateLineMetrics(); _textLineMetrics = CreateLineMetrics();
BidiReorder();
return this; return this;
} }
@ -1026,7 +1031,7 @@ namespace Avalonia.Media.TextFormatting
var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface; var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface;
var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize;
var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight; var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight;
var width = 0d; var width = 0d;
var widthIncludingWhitespace = 0d; var widthIncludingWhitespace = 0d;
var trailingWhitespaceLength = 0; var trailingWhitespaceLength = 0;
@ -1036,8 +1041,8 @@ namespace Avalonia.Media.TextFormatting
var lineGap = glyphTypeface.LineGap * scale; var lineGap = glyphTypeface.LineGap * scale;
var height = descent - ascent + lineGap; var height = descent - ascent + lineGap;
var lineHeight = _paragraphProperties.LineHeight; var lineHeight = _paragraphProperties.LineHeight;
for (var index = 0; index < _textRuns.Count; index++) for (var index = 0; index < _textRuns.Count; index++)
{ {
@ -1073,41 +1078,11 @@ namespace Avalonia.Media.TextFormatting
} }
} }
switch (_paragraphProperties.FlowDirection) if (index == _textRuns.Count - 1)
{ {
case FlowDirection.LeftToRight: width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
{ trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
if (index == _textRuns.Count - 1) newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
{
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
}
break;
}
case FlowDirection.RightToLeft:
{
if (index == _textRuns.Count - 1)
{
var firstRun = _textRuns[0];
if (firstRun is ShapedTextCharacters shapedTextCharacters)
{
var offset = shapedTextCharacters.GlyphRun.Metrics.WidthIncludingTrailingWhitespace -
shapedTextCharacters.GlyphRun.Metrics.Width;
width = widthIncludingWhitespace +
textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace - offset;
trailingWhitespaceLength = shapedTextCharacters.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength = shapedTextCharacters.GlyphRun.Metrics.NewlineLength;
}
}
break;
}
} }
widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace; widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
@ -1166,10 +1141,10 @@ namespace Avalonia.Media.TextFormatting
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
{ {
if(lineHeight > height) if (lineHeight > height)
{ {
height = lineHeight; height = lineHeight;
} }
} }
return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start, return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,

25
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -511,28 +511,39 @@ namespace Avalonia.Controls.Presenters
InvalidateMeasure(); InvalidateMeasure();
} }
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
if (string.IsNullOrEmpty(Text))
{
return new Size();
}
_constraint = availableSize; _constraint = availableSize;
_textLayout = null; _textLayout = null;
InvalidateArrange(); InvalidateArrange();
var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1); var measuredSize = TextLayout.Bounds.Size;
return new Size(measuredSize.Width, measuredSize.Height); return measuredSize;
} }
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
if (finalSize.Width < TextLayout.Bounds.Width)
{
finalSize = finalSize.WithWidth(TextLayout.Bounds.Width);
}
if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
{ {
return finalSize; return finalSize;
} }
_constraint = new Size(finalSize.Width, Math.Ceiling(finalSize.Height)); _constraint = new Size(finalSize.Width, double.PositiveInfinity);
_textLayout = null; _textLayout = null;

19
src/Avalonia.Controls/TextBlock.cs

@ -604,7 +604,9 @@ namespace Avalonia.Controls
return new Size(); return new Size();
} }
var padding = Padding; var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
_constraint = availableSize.Deflate(padding); _constraint = availableSize.Deflate(padding);
@ -612,23 +614,24 @@ namespace Avalonia.Controls
InvalidateArrange(); InvalidateArrange();
var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1); var measuredSize = TextLayout.Bounds.Size.Inflate(padding);
return new Size(measuredSize.Width, measuredSize.Height).Inflate(padding); return measuredSize;
} }
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
if(finalSize.Width < TextLayout.Bounds.Width)
{
finalSize = finalSize.WithWidth(TextLayout.Bounds.Width);
}
if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
{ {
return finalSize; return finalSize;
} }
var padding = Padding; _constraint = new Size(finalSize.Width, double.PositiveInfinity);
var textSize = finalSize.Deflate(padding);
_constraint = new Size(textSize.Width, Math.Ceiling(textSize.Height));
_textLayout = null; _textLayout = null;

7
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -27,7 +27,7 @@ namespace Avalonia.Skia
buffer.GuessSegmentProperties(); buffer.GuessSegmentProperties();
buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; buffer.Direction = Direction.LeftToRight; //Always shape LeftToRight
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
@ -35,11 +35,6 @@ namespace Avalonia.Skia
font.Shape(buffer); font.Shape(buffer);
if (buffer.Direction == Direction.RightToLeft)
{
buffer.Reverse();
}
font.GetScale(out var scaleX, out _); font.GetScale(out var scaleX, out _);
var textScale = fontRenderingEmSize / scaleX; var textScale = fontRenderingEmSize / scaleX;

33
src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Globalization; using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform; using Avalonia.Platform;
@ -11,8 +10,7 @@ using GlyphInfo = HarfBuzzSharp.GlyphInfo;
namespace Avalonia.Direct2D1.Media namespace Avalonia.Direct2D1.Media
{ {
internal class TextShaperImpl : ITextShaperImpl
internal class TextShaperImpl : ITextShaperImpl
{ {
public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options) public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
{ {
@ -23,25 +21,20 @@ internal class TextShaperImpl : ITextShaperImpl
using (var buffer = new Buffer()) using (var buffer = new Buffer())
{ {
buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length); buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length);
MergeBreakPair(buffer); MergeBreakPair(buffer);
buffer.GuessSegmentProperties(); buffer.GuessSegmentProperties();
buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; buffer.Direction = Direction.LeftToRight; //Always shape LeftToRight
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
var font = ((GlyphTypefaceImpl)typeface.PlatformImpl).Font; var font = ((GlyphTypefaceImpl)typeface.PlatformImpl).Font;
font.Shape(buffer); font.Shape(buffer);
if (buffer.Direction == Direction.RightToLeft)
{
buffer.Reverse();
}
font.GetScale(out var scaleX, out _); font.GetScale(out var scaleX, out _);
var textScale = fontRenderingEmSize / scaleX; var textScale = fontRenderingEmSize / scaleX;
@ -60,13 +53,13 @@ internal class TextShaperImpl : ITextShaperImpl
var glyphIndex = (ushort)sourceInfo.Codepoint; var glyphIndex = (ushort)sourceInfo.Codepoint;
var glyphCluster = (int)sourceInfo.Cluster; var glyphCluster = (int)(sourceInfo.Cluster);
var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale); var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale);
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
if (glyphIndex == 0 && text[glyphCluster] == '\t') if (glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t')
{ {
glyphIndex = typeface.GetGlyph(' '); glyphIndex = typeface.GetGlyph(' ');
@ -75,9 +68,7 @@ internal class TextShaperImpl : ITextShaperImpl
4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
} }
var targetInfo = var targetInfo = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance,
glyphOffset);
shapedBuffer[i] = targetInfo; shapedBuffer[i] = targetInfo;
} }
@ -91,7 +82,7 @@ internal class TextShaperImpl : ITextShaperImpl
var length = buffer.Length; var length = buffer.Length;
var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphInfos = buffer.GetGlyphInfoSpan();
var second = glyphInfos[length - 1]; var second = glyphInfos[length - 1];
if (!new Codepoint((int)second.Codepoint).IsBreakChar) if (!new Codepoint((int)second.Codepoint).IsBreakChar)
@ -102,7 +93,7 @@ internal class TextShaperImpl : ITextShaperImpl
if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n') if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n')
{ {
var first = glyphInfos[length - 2]; var first = glyphInfos[length - 2];
first.Codepoint = '\u200C'; first.Codepoint = '\u200C';
second.Codepoint = '\u200C'; second.Codepoint = '\u200C';
second.Cluster = first.Cluster; second.Cluster = first.Cluster;
@ -113,7 +104,7 @@ internal class TextShaperImpl : ITextShaperImpl
{ {
*p = first; *p = first;
} }
fixed (GlyphInfo* p = &glyphInfos[length - 1]) fixed (GlyphInfo* p = &glyphInfos[length - 1])
{ {
*p = second; *p = second;
@ -148,7 +139,7 @@ internal class TextShaperImpl : ITextShaperImpl
private static double GetGlyphAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale) private static double GetGlyphAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale)
{ {
// Depends on direction of layout // Depends on direction of layout
// advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale; // glyphPositions[index].YAdvance * textScale;
return glyphPositions[index].XAdvance * textScale; return glyphPositions[index].XAdvance * textScale;
} }
} }

BIN
tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 752 B

BIN
tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 B

After

Width:  |  Height:  |  Size: 557 B

Loading…
Cancel
Save