Browse Source

Only calculate glyph advances for non fixed pitch fonts

pull/3690/head
Benedikt Schroeder 6 years ago
parent
commit
0ca4c77acc
  1. 34
      src/Avalonia.Controls/TextBlock.cs
  2. 46
      src/Avalonia.Visuals/Media/GlyphRun.cs
  3. 89
      src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs
  4. 15
      src/Avalonia.Visuals/Utility/ReadOnlySlice.cs
  5. 26
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  6. 72
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  7. BIN
      tests/Avalonia.RenderTests/Assets/NotoSans-Italic.ttf
  8. 5
      tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs
  9. 66
      tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs

34
src/Avalonia.Controls/TextBlock.cs

@ -117,28 +117,18 @@ namespace Avalonia.Controls
{
ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true);
AffectsRender<TextBlock>(
BackgroundProperty, ForegroundProperty, FontSizeProperty,
FontWeightProperty, FontStyleProperty, TextWrappingProperty,
TextTrimmingProperty, TextAlignmentProperty, FontFamilyProperty,
TextDecorationsProperty, TextProperty, PaddingProperty);
AffectsMeasure<TextBlock>(
FontSizeProperty, FontWeightProperty, FontStyleProperty,
FontFamilyProperty, TextTrimmingProperty, TextProperty,
PaddingProperty);
Observable.Merge(
TextProperty.Changed,
ForegroundProperty.Changed,
TextAlignmentProperty.Changed,
TextWrappingProperty.Changed,
TextTrimmingProperty.Changed,
FontSizeProperty.Changed,
FontStyleProperty.Changed,
FontWeightProperty.Changed,
FontFamilyProperty.Changed,
TextDecorationsProperty.Changed,
AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty,
TextAlignmentProperty, TextDecorationsProperty);
AffectsMeasure<TextBlock>(FontSizeProperty, FontWeightProperty,
FontStyleProperty, TextWrappingProperty, FontFamilyProperty,
TextTrimmingProperty, TextProperty, PaddingProperty);
Observable.Merge(TextProperty.Changed, ForegroundProperty.Changed,
TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
TextTrimmingProperty.Changed, FontSizeProperty.Changed,
FontStyleProperty.Changed, FontWeightProperty.Changed,
FontFamilyProperty.Changed, TextDecorationsProperty.Changed,
PaddingProperty.Changed
).AddClassHandler<TextBlock>((x, _) => x.InvalidateTextLayout());
}

46
src/Avalonia.Visuals/Media/GlyphRun.cs

@ -274,27 +274,39 @@ namespace Avalonia.Media
var currentX = 0.0;
var index = 0;
for (; index < GlyphIndices.Length; index++)
if (GlyphTypeface.IsFixedPitch)
{
double advance;
var glyph = GlyphIndices[index];
if (GlyphAdvances.IsEmpty)
{
var glyph = GlyphIndices[index];
var advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
else
index = Math.Min(GlyphIndices.Length - 1,
(int)Math.Round(distance / advance, MidpointRounding.AwayFromZero));
}
else
{
for (; index < GlyphIndices.Length; index++)
{
advance = GlyphAdvances[index];
}
double advance;
if (currentX + advance >= distance)
{
break;
}
if (GlyphAdvances.IsEmpty)
{
var glyph = GlyphIndices[index];
advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
else
{
advance = GlyphAdvances[index];
}
currentX += advance;
if (currentX + advance >= distance)
{
break;
}
currentX += advance;
}
}
var characterHit = FindNearestCharacterHit(GlyphClusters[index], out var width);
@ -473,9 +485,7 @@ namespace Avalonia.Media
/// </returns>
private Rect CalculateBounds()
{
var scale = FontRenderingEmSize / GlyphTypeface.DesignEmHeight;
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * scale;
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
var width = 0.0;

89
src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs

@ -10,15 +10,7 @@ namespace Avalonia.Media.TextFormatting
{
private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
/// <summary>
/// Formats a text line.
/// </summary>
/// <param name="textSource">The text source.</param>
/// <param name="firstTextSourceIndex">The first character index to start the text line from.</param>
/// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
/// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
/// such as TextWrapping, TextAlignment, or TextStyle.</param>
/// <returns>The formatted line.</returns>
/// <inheritdoc cref="TextFormatter.FormatLine"/>
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties)
{
@ -61,17 +53,18 @@ namespace Avalonia.Media.TextFormatting
/// </returns>
private List<ShapedTextRun> FormatTextRuns(ITextSource textSource, int firstTextSourceIndex, out TextPointer textPointer)
{
var start = firstTextSourceIndex;
var start = -1;
var length = 0;
var textRuns = new List<ShapedTextRun>();
while (true)
{
var textRun = textSource.GetTextRun(firstTextSourceIndex);
var textRun = textSource.GetTextRun(firstTextSourceIndex + length);
if (textRun.Text.IsEmpty)
if (start == -1)
{
break;
start = textRun.Text.Start;
}
if (textRun is TextEndOfLine)
@ -79,29 +72,33 @@ namespace Avalonia.Media.TextFormatting
break;
}
if (!(textRun is TextCharacters))
switch (textRun)
{
throw new NotSupportedException("Run type not supported by the formatter.");
}
case TextCharacters textCharacters:
var runText = textRun.Text;
var runText = textCharacters.Text;
while (!runText.IsEmpty)
{
var shapableTextStyleRun = CreateShapableTextStyleRun(runText, textRun.Style);
while (!runText.IsEmpty)
{
var shapableTextStyleRun = CreateShapableTextStyleRun(runText, textRun.Style);
var shapedRun = new ShapedTextRun(runText.Take(shapableTextStyleRun.TextPointer.Length),
shapableTextStyleRun.Style);
var shapedRun = new ShapedTextRun(runText.Take(shapableTextStyleRun.TextPointer.Length),
shapableTextStyleRun.Style);
textRuns.Add(shapedRun);
textRuns.Add(shapedRun);
runText = runText.Skip(shapedRun.Text.Length);
runText = runText.Skip(shapedRun.Text.Length);
}
break;
default:
throw new NotSupportedException("Run type not supported by the formatter.");
}
firstTextSourceIndex += textRun.Text.Length;
length += textRun.Text.Length;
}
textPointer = new TextPointer(start, firstTextSourceIndex - start);
textPointer = new TextPointer(start, length);
return textRuns;
}
@ -115,7 +112,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textRuns">The text runs to perform the trimming on.</param>
/// <param name="text">The text that was used to construct the text runs.</param>
/// <returns></returns>
private TextLine PerformTextTrimming(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
private static TextLine PerformTextTrimming(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
double paragraphWidth, TextParagraphProperties paragraphProperties)
{
var textTrimming = paragraphProperties.TextTrimming;
@ -195,7 +192,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="text">The text to analyze for break opportunities.</param>
/// <param name="paragraphWidth"></param>
/// <returns></returns>
private TextLine PerformTextWrapping(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
private static TextLine PerformTextWrapping(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
double paragraphWidth, TextParagraphProperties paragraphProperties)
{
var availableWidth = paragraphWidth;
@ -267,41 +264,13 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textRun">The text run.</param>
/// <param name="availableWidth">The available width.</param>
/// <returns></returns>
private int MeasureText(ShapedTextRun textRun, double availableWidth)
private static int MeasureText(ShapedTextRun textRun, double availableWidth)
{
if (textRun.GlyphRun.Bounds.Width < availableWidth)
{
return textRun.Text.Length;
}
var measuredWidth = 0.0;
var index = 0;
for (; index < textRun.GlyphRun.GlyphAdvances.Length; index++)
{
var advance = textRun.GlyphRun.GlyphAdvances[index];
if (measuredWidth + advance > availableWidth)
{
index--;
break;
}
measuredWidth += advance;
}
if(index < 0)
{
return 0;
}
var cluster = textRun.GlyphRun.GlyphClusters[index];
var glyphRun = textRun.GlyphRun;
var characterHit = textRun.GlyphRun.FindNearestCharacterHit(cluster, out _);
var characterHit = glyphRun.GetCharacterHitFromDistance(availableWidth, out _);
return characterHit.FirstCharacterIndex - textRun.GlyphRun.Characters.Start +
(textRun.GlyphRun.IsLeftToRight ? characterHit.TrailingLength : 0);
return characterHit.FirstCharacterIndex + characterHit.TrailingLength - textRun.Text.Start;
}
/// <summary>

15
src/Avalonia.Visuals/Utility/ReadOnlySlice.cs

@ -69,6 +69,11 @@ namespace Avalonia.Utility
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the specified start.</returns>
public ReadOnlySlice<T> AsSlice(int start, int length)
{
if (IsEmpty)
{
return this;
}
if (start < Start || start > End)
{
throw new ArgumentOutOfRangeException(nameof(start));
@ -91,6 +96,11 @@ namespace Avalonia.Utility
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the start of this slice.</returns>
public ReadOnlySlice<T> Take(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
@ -106,6 +116,11 @@ namespace Avalonia.Utility
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the elements that occur after the specified index in this slice.</returns>
public ReadOnlySlice<T> Skip(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));

26
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -173,16 +173,26 @@ namespace Avalonia.Skia
using (var textBlobBuilder = new SKTextBlobBuilder())
{
SKTextBlob textBlob;
width = 0;
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
if (glyphRun.GlyphOffsets.IsEmpty)
{
width = 0;
if (glyphTypeface.IsFixedPitch)
{
textBlobBuilder.AddRun(paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0);
textBlob = textBlobBuilder.Build();
if (!glyphTypeface.IsFixedPitch)
width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
}
else
{
var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0);
var positions = buffer.GetPositionSpan();
for (var i = 0; i < count; i++)
@ -198,9 +208,11 @@ namespace Avalonia.Skia
width += glyphRun.GlyphAdvances[i];
}
}
}
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
textBlob = textBlobBuilder.Build();
}
}
else
{
@ -229,9 +241,9 @@ namespace Avalonia.Skia
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
width = currentX;
}
var textBlob = textBlobBuilder.Build();
textBlob = textBlobBuilder.Build();
}
return new GlyphRunImpl(paint, textBlob);
}

72
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -1,4 +1,5 @@
using Avalonia.Media;
using System;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
@ -72,36 +73,32 @@ namespace Avalonia.Skia
var textScale = textFormat.FontRenderingEmSize / scaleX;
var len = buffer.Length;
var bufferLength = buffer.Length;
var info = buffer.GetGlyphInfoSpan();
var glyphInfos = buffer.GetGlyphInfoSpan();
var pos = buffer.GetGlyphPositionSpan();
var glyphPositions = buffer.GetGlyphPositionSpan();
var glyphIndices = new ushort[len];
var glyphIndices = new ushort[bufferLength];
var clusters = new ushort[len];
var clusters = new ushort[bufferLength];
var glyphAdvances = new double[len];
double[] glyphAdvances = null;
var glyphOffsets = new Vector[len];
Vector[] glyphOffsets = null;
for (var i = 0; i < len; i++)
for (var i = 0; i < bufferLength; i++)
{
glyphIndices[i] = (ushort)info[i].Codepoint;
glyphIndices[i] = (ushort)glyphInfos[i].Codepoint;
clusters[i] = (ushort)(text.Start + info[i].Cluster);
clusters[i] = (ushort)(text.Start + glyphInfos[i].Cluster);
var advanceX = pos[i].XAdvance * textScale;
// Depends on direction of layout
//var advanceY = pos[i].YAdvance * textScale;
glyphAdvances[i] = advanceX;
var offsetX = pos[i].XOffset * textScale;
var offsetY = pos[i].YOffset * textScale;
if (!glyphTypeface.IsFixedPitch)
{
SetAdvance(glyphPositions, i, textScale, ref glyphAdvances);
}
glyphOffsets[i] = new Vector(offsetX, offsetY);
SetOffset(glyphPositions, i, textScale, ref glyphOffsets);
}
return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize,
@ -112,5 +109,40 @@ namespace Avalonia.Skia
new ReadOnlySlice<ushort>(clusters));
}
}
private static void SetOffset(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale,
ref Vector[] offsetBuffer)
{
var position = glyphPositions[index];
if (position.XOffset == 0 && position.YOffset == 0)
{
return;
}
if (offsetBuffer == null)
{
offsetBuffer = new Vector[glyphPositions.Length];
}
var offsetX = position.XOffset * textScale;
var offsetY = position.YOffset * textScale;
offsetBuffer[index] = new Vector(offsetX, offsetY);
}
private static void SetAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale,
ref double[] advanceBuffer)
{
if (advanceBuffer == null)
{
advanceBuffer = new double[glyphPositions.Length];
}
// Depends on direction of layout
// advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale;
advanceBuffer[index] = glyphPositions[index].XAdvance * textScale;
}
}
}

BIN
tests/Avalonia.RenderTests/Assets/NotoSans-Italic.ttf

Binary file not shown.

5
tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs

@ -14,12 +14,14 @@ namespace Avalonia.Skia.UnitTests
private readonly Typeface _defaultTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
private readonly Typeface _italicTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans");
private readonly Typeface _emojiTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji");
public CustomFontManagerImpl()
{
_customTypefaces = new[] { _emojiTypeface, _defaultTypeface };
_customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface };
}
public string GetDefaultFontFamilyName()
@ -56,6 +58,7 @@ namespace Avalonia.Skia.UnitTests
switch (typeface.FontFamily.Name)
{
case "Twitter Color Emoji":
case "Noto Sans":
case "Noto Mono":
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
var skTypeface = typefaceCollection.Get(typeface);

66
tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.UnitTests;
using Avalonia.Utility;
using Xunit;
@ -240,7 +242,9 @@ namespace Avalonia.Skia.UnitTests
{
var cluster = glyphRun.GlyphClusters[i];
var advance = glyphRun.GlyphAdvances[i];
var glyph = glyphRun.GlyphIndices[i];
var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster));
@ -280,7 +284,9 @@ namespace Avalonia.Skia.UnitTests
{
var cluster = glyphRun.GlyphClusters[i];
var advance = glyphRun.GlyphAdvances[i];
var glyph = glyphRun.GlyphIndices[i];
var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
characterHit = textLine.GetCharacterHitFromDistance(currentDistance);
@ -296,6 +302,62 @@ namespace Avalonia.Skia.UnitTests
}
}
[InlineData("Whether to turn off HTTPS. This option only applies if Individual, " +
"IndividualB2C, SingleOrg, or MultiOrg aren't used for &#8209;&#8209;auth."
, "Noto Sans", 40)]
[InlineData("01234 56789 01234 56789", "Noto Mono", 7)]
[Theory]
public void Should_Wrap_Text(string text, string familyName, int numberOfCharactersPerLine)
{
using (Start())
{
var lineBreaker = new LineBreakEnumerator(text.AsMemory());
var expected = new List<int>();
while (lineBreaker.MoveNext())
{
expected.Add(lineBreaker.Current.PositionWrap - 1);
}
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" +
familyName);
var defaultStyle = new TextStyle(typeface);
var textSource = new SimpleTextSource(text, defaultStyle);
var formatter = new SimpleTextFormatter();
var glyph = typeface.GlyphTypeface.GetGlyph('a');
var advance = typeface.GlyphTypeface.GetGlyphAdvance(glyph) *
(12.0 / typeface.GlyphTypeface.DesignEmHeight);
var paragraphWidth = advance * numberOfCharactersPerLine;
var currentPosition = 0;
while (currentPosition < text.Length)
{
var textLine =
formatter.FormatLine(textSource, currentPosition, paragraphWidth,
new TextParagraphProperties(defaultStyle, textWrapping: TextWrapping.Wrap));
Assert.True(expected.Contains(textLine.Text.End));
var index = expected.IndexOf(textLine.Text.End);
for (var i = 0; i <= index; i++)
{
expected.RemoveAt(0);
}
currentPosition += textLine.Text.Length;
}
}
}
public static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface

Loading…
Cancel
Save