Browse Source

Fix font fallback

Fix TextPresenter measure
pull/4893/head
Benedikt Stebner 5 years ago
parent
commit
3fdfdd64d2
  1. 4
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  2. 10
      src/Avalonia.Visuals/Media/GlyphRun.cs
  3. 2
      src/Avalonia.Visuals/Media/GlyphTypeface.cs
  4. 9
      src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
  5. 76
      src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
  6. 7
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  7. 2
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  8. 32
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

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

@ -515,9 +515,11 @@ namespace Avalonia.Controls.Presenters
protected override Size MeasureOverride(Size availableSize)
{
if (availableSize != Size.Infinity)
if (!double.IsInfinity(availableSize.Width) && availableSize != _constraint)
{
_constraint = availableSize;
InvalidateTextLayout();
}
return TextLayout.Size;

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

@ -669,6 +669,15 @@ namespace Avalonia.Media
var codepointIndex = IsLeftToRight ? cluster - _characters.Start : _characters.End - cluster;
if (codepointIndex < 0)
{
trailingWhitespaceLength = _characters.Length;
glyphCount = GlyphClusters.Count;
break;
}
var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _);
if (!codepoint.IsWhiteSpace)
@ -682,6 +691,7 @@ namespace Avalonia.Media
}
trailingWhitespaceLength++;
glyphCount++;
}
}

2
src/Avalonia.Visuals/Media/GlyphTypeface.cs

@ -5,8 +5,6 @@ namespace Avalonia.Media
{
public sealed class GlyphTypeface : IDisposable
{
public const int InvisibleGlyph = 3;
public GlyphTypeface(Typeface typeface)
: this(FontManager.Current.PlatformImpl.CreateGlyphTypeface(typeface))
{

9
src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs

@ -152,6 +152,15 @@ namespace Avalonia.Media.TextFormatting
var first = new ShapedTextCharacters(splitBuffer.First, Properties);
#if DEBUG
if (first.Text.Length != length)
{
throw new InvalidOperationException("Split length mismatch.");
}
#endif
var second = new ShapedTextCharacters(splitBuffer.Second!, Properties);
return new SplitResult<ShapedTextCharacters>(first, second);

76
src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
@ -37,17 +38,20 @@ namespace Avalonia.Media.TextFormatting
/// Gets a list of <see cref="ShapeableTextCharacters"/>.
/// </summary>
/// <returns>The shapeable text characters.</returns>
internal IList<ShapeableTextCharacters> GetShapeableCharacters(ReadOnlySlice<char> runText, sbyte biDiLevel)
internal IList<ShapeableTextCharacters> GetShapeableCharacters(ReadOnlySlice<char> runText, sbyte biDiLevel,
ref TextRunProperties? previousProperties)
{
var shapeableCharacters = new List<ShapeableTextCharacters>(2);
while (!runText.IsEmpty)
{
var shapeableRun = CreateShapeableRun(runText, Properties, biDiLevel);
var shapeableRun = CreateShapeableRun(runText, Properties, biDiLevel, ref previousProperties);
shapeableCharacters.Add(shapeableRun);
runText = runText.Skip(shapeableRun.Text.Length);
previousProperties = shapeableRun.Properties;
}
return shapeableCharacters;
@ -59,15 +63,29 @@ namespace Avalonia.Media.TextFormatting
/// <param name="text">The text to create text runs from.</param>
/// <param name="defaultProperties">The default text run properties.</param>
/// <param name="biDiLevel">The bidi level of the run.</param>
/// <param name="previousProperties"></param>
/// <returns>A list of shapeable text runs.</returns>
private ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice<char> text, TextRunProperties defaultProperties, sbyte biDiLevel)
private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice<char> text,
TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
{
var defaultTypeface = defaultProperties.Typeface;
var currentTypeface = defaultTypeface;
if (TryGetShapeableLength(text, currentTypeface, defaultTypeface, out var count))
if (TryGetShapeableLength(text, currentTypeface, out var count, out var script))
{
var previousTypeface = previousProperties?.Typeface;
if (script == Script.Common && previousTypeface is not null)
{
if(TryGetShapeableLength(text, previousTypeface.Value, out var fallbackCount, out _))
{
return new ShapeableTextCharacters(text.Take(fallbackCount),
new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize,
defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
}
}
return new ShapeableTextCharacters(text.Take(count),
new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
@ -94,7 +112,7 @@ namespace Avalonia.Media.TextFormatting
FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface);
if (matchFound && TextCharacters.TryGetShapeableLength(text, currentTypeface, defaultTypeface, out count))
if (matchFound && TryGetShapeableLength(text, currentTypeface, out count, out _))
{
//Fallback found
return new ShapeableTextCharacters(text.Take(count),
@ -127,30 +145,26 @@ namespace Avalonia.Media.TextFormatting
}
/// <summary>
/// Tries to get run properties.
/// Tries to get a shapeable length that is supported by the specified typeface.
/// </summary>
/// <param name="defaultTypeface"></param>
/// <param name="text"></param>
/// <param name="text">The text.</param>
/// <param name="typeface">The typeface that is used to find matching characters.</param>
/// <param name="length"></param>
/// <param name="length">The shapeable length.</param>
/// <param name="script"></param>
/// <returns></returns>
protected static bool TryGetShapeableLength(ReadOnlySlice<char> text, Typeface typeface, Typeface defaultTypeface,
out int length)
protected static bool TryGetShapeableLength(ReadOnlySlice<char> text, Typeface typeface, out int length,
out Script script)
{
length = 0;
script = Script.Unknown;
if (text.Length == 0)
{
length = 0;
return false;
}
var isFallback = typeface != defaultTypeface;
length = 0;
var script = Script.Unknown;
var font = typeface.GlyphTypeface;
var defaultFont = defaultTypeface.GlyphTypeface;
var enumerator = new GraphemeEnumerator(text);
while (enumerator.MoveNext())
@ -161,7 +175,8 @@ namespace Avalonia.Media.TextFormatting
if (currentScript != script)
{
if (script is Script.Unknown || currentScript != Script.Common && (script is Script.Common || script is Script.Inherited))
if (script is Script.Unknown || currentScript != Script.Common &&
(script is Script.Common || script is Script.Inherited))
{
script = currentScript;
}
@ -174,23 +189,8 @@ namespace Avalonia.Media.TextFormatting
}
}
//Only handle non whitespace here
if(!currentGrapheme.FirstCodepoint.IsWhiteSpace)
{
//Stop at the first glyph that is present in the default typeface.
if (isFallback && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
{
break;
}
//Stop at the first missing glyph
if (!font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
{
break;
}
}
if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
//Stop at the first missing glyph
if (!font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
{
break;
}

7
src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs

@ -208,6 +208,7 @@ namespace Avalonia.Media.TextFormatting
var levelIndex = 0;
var runLevel = levels[0];
TextRunProperties? previousProperties = null;
TextCharacters? currentRun = null;
var runText = ReadOnlySlice<char>.Empty;
@ -231,7 +232,7 @@ namespace Avalonia.Media.TextFormatting
if (j == runText.Length)
{
yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel);
yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties);
runLevel = levels[levelIndex];
@ -244,7 +245,7 @@ namespace Avalonia.Media.TextFormatting
}
// End of this run
yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel);
yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties);
runText = runText.Skip(j);
@ -260,7 +261,7 @@ namespace Avalonia.Media.TextFormatting
yield break;
}
yield return currentRun.GetShapeableCharacters(runText, runLevel);
yield return currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties);
}
/// <summary>

2
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -64,6 +64,8 @@ namespace Avalonia.Skia
public SKTypeface Typeface { get; }
public int ReplacementCodepoint { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public short DesignEmHeight { get; }

32
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -461,6 +461,38 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(expectedOffset, textLine.Start);
}
}
[Fact]
public void Should_Wrap_Syriac()
{
using (Start())
{
const string text =
"܀ ܁ ܂ ܃ ܄ ܅ ܆ ܇ ܈ ܉ ܊ ܋ ܌ ܍ ܏ ܐ ܑ ܒ ܓ ܔ ܕ ܖ ܗ ܘ ܙ ܚ ܛ ܜ ܝ ܞ ܟ ܠ ܡ ܢ ܣ ܤ ܥ ܦ ܧ ܨ ܩ ܪ ܫ ܬ ܰ ܱ ܲ ܳ ܴ ܵ ܶ ܷ ܸ ܹ ܺ ܻ ܼ ܽ ܾ ܿ ݀ ݁ ݂ ݃ ݄ ݅ ݆ ݇ ݈ ݉ ݊";
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var paragraphProperties =
new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
var textPosition = 87;
TextLineBreak lastBreak = null;
while (textPosition < text.Length)
{
var textLine =
formatter.FormatLine(textSource, textPosition, 50, paragraphProperties, lastBreak);
Assert.Equal(textLine.TextRange.Length, textLine.TextRuns.Sum(x => x.TextSourceLength));
textPosition += textLine.TextRange.Length;
lastBreak = textLine.TextLineBreak;
}
}
}
[Fact]
public void Should_FormatLine_With_Emergency_Breaks()

Loading…
Cancel
Save