Browse Source

Merge pull request #9464 from Gillibald/characterBufferReference

Port CharacterBufferReference
pull/9665/head
Max Katz 3 years ago
committed by GitHub
parent
commit
946078abf4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      samples/RenderDemo/Pages/TextFormatterPage.axaml.cs
  2. 8
      src/Avalonia.Base/Media/FormattedText.cs
  3. 419
      src/Avalonia.Base/Media/GlyphRun.cs
  4. 18
      src/Avalonia.Base/Media/GlyphRunMetrics.cs
  5. 293
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
  6. 115
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs
  7. 13
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  8. 19
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  9. 20
      src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs
  10. 31
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  11. 19
      src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs
  12. 2
      src/Avalonia.Base/Media/TextFormatting/SplitResult.cs
  13. 129
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  14. 102
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  15. 4
      src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs
  16. 100
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  17. 2
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  18. 5
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  19. 358
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  20. 6
      src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs
  21. 4
      src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs
  22. 11
      src/Avalonia.Base/Media/TextFormatting/TextRun.cs
  23. 11
      src/Avalonia.Base/Media/TextFormatting/TextShaper.cs
  24. 3
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  25. 2
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  26. 3
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  27. 9
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  28. 7
      src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  29. 8
      src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
  30. 12
      src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
  31. 15
      src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
  32. 11
      src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs
  33. 11
      src/Avalonia.Base/Media/TextTrailingTrimming.cs
  34. 2
      src/Avalonia.Base/Media/TextTrimming.cs
  35. 5
      src/Avalonia.Base/Platform/ITextShaperImpl.cs
  36. 3
      src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs
  37. 8
      src/Avalonia.Base/Utilities/ArraySlice.cs
  38. 239
      src/Avalonia.Base/Utilities/ReadOnlySlice.cs
  39. 4
      src/Avalonia.Controls/Documents/LineBreak.cs
  40. 2
      src/Avalonia.Controls/Documents/Run.cs
  41. 27
      src/Avalonia.Controls/TextBlock.cs
  42. 4
      src/Avalonia.Controls/TextBox.cs
  43. 8
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  44. 6
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  45. 24
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  46. 11
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  47. 4
      tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
  48. 3
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs
  49. 9
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs
  50. 9
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs
  51. 37
      tests/Avalonia.Base.UnitTests/Utilities/ReadOnlySpanTests.cs
  52. 2
      tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs
  53. 29
      tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
  54. 3
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
  55. 19
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
  56. 21
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  57. 46
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  58. 90
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  59. 6
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs
  60. 8
      tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
  61. 12
      tests/Avalonia.UnitTests/MockTextShaperImpl.cs

2
samples/RenderDemo/Pages/TextFormatterPage.axaml.cs

@ -90,7 +90,7 @@ namespace RenderDemo.Pages
return new ControlRun(_control, _defaultProperties);
}
return new TextCharacters(_text.AsMemory(), _defaultProperties);
return new TextCharacters(_text, _defaultProperties);
}
}

8
src/Avalonia.Base/Media/FormattedText.cs

@ -1,10 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using Avalonia.Controls;
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
@ -25,7 +23,7 @@ namespace Avalonia.Media
private const double MaxFontEmSize = RealInfiniteWidth / GreatestMultiplierOfEm;
// properties and format runs
private ReadOnlySlice<char> _text;
private string _text;
private readonly SpanVector _formatRuns = new SpanVector(null);
private SpanPosition _latestPosition;
@ -69,9 +67,7 @@ namespace Avalonia.Media
ValidateFontSize(emSize);
_text = textToFormat != null ?
new ReadOnlySlice<char>(textToFormat.AsMemory()) :
throw new ArgumentNullException(nameof(textToFormat));
_text = textToFormat;
var runProps = new GenericTextRunProperties(
typeface,

419
src/Avalonia.Base/Media/GlyphRun.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
@ -22,15 +21,12 @@ namespace Avalonia.Media
private Point? _baselineOrigin;
private GlyphRunMetrics? _glyphRunMetrics;
private ReadOnlySlice<char> _characters;
private IReadOnlyList<char> _characters;
private IReadOnlyList<ushort> _glyphIndices;
private IReadOnlyList<double>? _glyphAdvances;
private IReadOnlyList<Vector>? _glyphOffsets;
private IReadOnlyList<int>? _glyphClusters;
private int _offsetToFirstCharacter;
/// <summary>
/// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
/// </summary>
@ -45,7 +41,7 @@ namespace Avalonia.Media
public GlyphRun(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
ReadOnlySlice<char> characters,
IReadOnlyList<char> characters,
IReadOnlyList<ushort> glyphIndices,
IReadOnlyList<double>? glyphAdvances = null,
IReadOnlyList<Vector>? glyphOffsets = null,
@ -54,19 +50,19 @@ namespace Avalonia.Media
{
_glyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
_fontRenderingEmSize = fontRenderingEmSize;
Characters = characters;
_characters = characters;
_glyphIndices = glyphIndices;
GlyphAdvances = glyphAdvances;
_glyphAdvances = glyphAdvances;
GlyphOffsets = glyphOffsets;
_glyphOffsets = glyphOffsets;
GlyphClusters = glyphClusters;
_glyphClusters = glyphClusters;
BiDiLevel = biDiLevel;
_biDiLevel = biDiLevel;
}
/// <summary>
@ -145,7 +141,7 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
/// </summary>
public ReadOnlySlice<char> Characters
public IReadOnlyList<char> Characters
{
get => _characters;
set => Set(ref _characters, value);
@ -219,7 +215,7 @@ namespace Avalonia.Media
/// </returns>
public double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength - _offsetToFirstCharacter;
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var distance = 0.0;
@ -227,12 +223,12 @@ namespace Avalonia.Media
{
if (GlyphClusters != null)
{
if (characterIndex < GlyphClusters[0])
if (characterIndex < Metrics.FirstCluster)
{
return 0;
}
if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
if (characterIndex > Metrics.LastCluster)
{
return Metrics.WidthIncludingTrailingWhitespace;
}
@ -268,12 +264,12 @@ namespace Avalonia.Media
if (GlyphClusters != null && GlyphClusters.Count > 0)
{
if (characterIndex > GlyphClusters[0])
if (characterIndex > Metrics.LastCluster)
{
return 0;
}
if (characterIndex <= GlyphClusters[GlyphClusters.Count - 1])
if (characterIndex <= Metrics.FirstCluster)
{
return Size.Width;
}
@ -299,19 +295,12 @@ namespace Avalonia.Media
/// </returns>
public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
{
var characterIndex = 0;
// Before
if (distance <= 0)
{
isInside = false;
if (GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
var firstCharacterHit = FindNearestCharacterHit(characterIndex, out _);
var firstCharacterHit = FindNearestCharacterHit(IsLeftToRight ? Metrics.FirstCluster : Metrics.LastCluster, out _);
return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit;
}
@ -321,18 +310,13 @@ namespace Avalonia.Media
{
isInside = false;
characterIndex = GlyphIndices.Count - 1;
if (GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _);
var lastCharacterHit = FindNearestCharacterHit(IsLeftToRight ? Metrics.LastCluster : Metrics.FirstCluster, out _);
return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
}
var characterIndex = 0;
//Within
var currentX = 0d;
@ -378,7 +362,7 @@ namespace Avalonia.Media
var characterHit = FindNearestCharacterHit(characterIndex, out var width);
var delta = width / 2;
var offset = IsLeftToRight ? Math.Round(distance - currentX, 3) : Math.Round(currentX - distance, 3);
var isTrailing = offset > delta;
@ -400,24 +384,15 @@ namespace Avalonia.Media
{
characterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
return textPosition > _characters.End ?
characterHit :
new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
var nextCharacterHit =
FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
if (characterHit.FirstCharacterIndex == Metrics.LastCluster)
{
return characterHit;
}
if (characterHit == nextCharacterHit)
{
return characterHit;
return new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
return characterHit.TrailingLength > 0 ?
nextCharacterHit :
new CharacterHit(nextCharacterHit.FirstCharacterIndex);
return FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
}
/// <summary>
@ -454,29 +429,24 @@ namespace Avalonia.Media
return characterIndex;
}
if (IsLeftToRight)
if (characterIndex > Metrics.LastCluster)
{
if (characterIndex < GlyphClusters[0])
if (IsLeftToRight)
{
return 0;
return GlyphIndices.Count - 1;
}
if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
{
return GlyphClusters.Count - 1;
}
return 0;
}
else
{
if (characterIndex < GlyphClusters[GlyphClusters.Count - 1])
{
return GlyphClusters.Count - 1;
}
if (characterIndex > GlyphClusters[0])
if (characterIndex < Metrics.FirstCluster)
{
if (IsLeftToRight)
{
return 0;
}
return GlyphIndices.Count - 1;
}
var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
@ -498,7 +468,7 @@ namespace Avalonia.Media
if (start < 0)
{
return -1;
goto result;
}
}
@ -517,6 +487,18 @@ namespace Avalonia.Media
}
}
result:
if (start < 0)
{
return 0;
}
if (start > GlyphIndices.Count - 1)
{
return GlyphIndices.Count - 1;
}
return start;
}
@ -532,20 +514,20 @@ namespace Avalonia.Media
{
width = 0.0;
var start = FindGlyphIndex(index);
var glyphIndex = FindGlyphIndex(index);
if (GlyphClusters == null)
{
width = GetGlyphAdvance(index, out _);
return new CharacterHit(start, 1);
return new CharacterHit(glyphIndex, 1);
}
var cluster = GlyphClusters[start];
var cluster = GlyphClusters[glyphIndex];
var nextCluster = cluster;
var currentIndex = start;
var currentIndex = glyphIndex;
while (nextCluster == cluster)
{
@ -571,20 +553,64 @@ namespace Avalonia.Media
}
nextCluster = GlyphClusters[currentIndex];
}
}
int trailingLength;
var clusterLength = Math.Max(0, nextCluster - cluster);
if (nextCluster == cluster)
{
trailingLength = Characters.Start + Characters.Length - _offsetToFirstCharacter - cluster;
}
else
if (cluster == Metrics.LastCluster && clusterLength == 0)
{
trailingLength = nextCluster - cluster;
var characterLength = 0;
var currentCluster = Metrics.FirstCluster;
if (IsLeftToRight)
{
for (int i = 1; i < GlyphClusters.Count; i++)
{
nextCluster = GlyphClusters[i];
if (currentCluster > cluster)
{
break;
}
var length = nextCluster - currentCluster;
characterLength += length;
currentCluster = nextCluster;
}
}
else
{
for (int i = GlyphClusters.Count - 1; i >= 0; i--)
{
nextCluster = GlyphClusters[i];
if (currentCluster > cluster)
{
break;
}
var length = nextCluster - currentCluster;
characterLength += length;
currentCluster = nextCluster;
}
}
if (Characters != null)
{
clusterLength = Characters.Count - characterLength;
}
else
{
clusterLength = 1;
}
}
return new CharacterHit(_offsetToFirstCharacter + cluster, trailingLength);
return new CharacterHit(cluster, clusterLength);
}
/// <summary>
@ -618,22 +644,25 @@ namespace Avalonia.Media
private GlyphRunMetrics CreateGlyphRunMetrics()
{
var firstCluster = 0;
var lastCluster = Characters.Length - 1;
int firstCluster = 0, lastCluster = 0;
if (!IsLeftToRight)
if (_glyphClusters != null && _glyphClusters.Count > 0)
{
var cluster = firstCluster;
firstCluster = lastCluster;
lastCluster = cluster;
firstCluster = _glyphClusters[0];
lastCluster = _glyphClusters[_glyphClusters.Count - 1];
}
if (GlyphClusters != null && GlyphClusters.Count > 0)
else
{
firstCluster = GlyphClusters[0];
lastCluster = GlyphClusters[GlyphClusters.Count - 1];
if (Characters != null && Characters.Count > 0)
{
firstCluster = 0;
lastCluster = Characters.Count - 1;
}
}
_offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster);
if (!IsLeftToRight)
{
(lastCluster, firstCluster) = (firstCluster, lastCluster);
}
var isReversed = firstCluster > lastCluster;
@ -666,12 +695,19 @@ namespace Avalonia.Media
}
}
return new GlyphRunMetrics(width, widthIncludingTrailingWhitespace, trailingWhitespaceLength, newLineLength,
height);
return new GlyphRunMetrics(
width,
widthIncludingTrailingWhitespace,
height,
trailingWhitespaceLength,
newLineLength,
firstCluster,
lastCluster
);
}
private int GetTrailingWhitespaceLength(bool isReversed, out int newLineLength, out int glyphCount)
{
{
if (isReversed)
{
return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount);
@ -681,66 +717,82 @@ namespace Avalonia.Media
newLineLength = 0;
var trailingWhitespaceLength = 0;
if (GlyphClusters == null)
if (Characters != null)
{
for (var i = _characters.Length - 1; i >= 0;)
if (GlyphClusters == null)
{
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
if (!codepoint.IsWhiteSpace)
for (var i = _characters.Count - 1; i >= 0;)
{
break;
}
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
if (codepoint.IsBreakChar)
{
newLineLength++;
}
if (!codepoint.IsWhiteSpace)
{
break;
}
trailingWhitespaceLength++;
if (codepoint.IsBreakChar)
{
newLineLength++;
}
trailingWhitespaceLength++;
i -= count;
glyphCount++;
i -= count;
glyphCount++;
}
}
}
else
{
for (var i = GlyphClusters.Count - 1; i >= 0; i--)
else
{
var currentCluster = GlyphClusters[i];
var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
if (!codepoint.IsWhiteSpace)
if (Characters.Count > 0)
{
break;
}
var characterIndex = Characters.Count - 1;
var clusterLength = 1;
for (var i = GlyphClusters.Count - 1; i >= 0; i--)
{
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength);
while(i - 1 >= 0)
{
var nextCluster = GlyphClusters[i - 1];
characterIndex -= characterLength;
if(currentCluster == nextCluster)
{
clusterLength++;
i--;
if (!codepoint.IsWhiteSpace)
{
break;
}
continue;
}
var clusterLength = 1;
break;
}
while (i - 1 >= 0)
{
var nextCluster = GlyphClusters[i - 1];
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
if (currentCluster == nextCluster)
{
clusterLength++;
i--;
if(characterIndex >= 0)
{
codepoint = Codepoint.ReadAt(_characters, characterIndex, out characterLength);
characterIndex -= characterLength;
}
continue;
}
break;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
trailingWhitespaceLength += clusterLength;
glyphCount++;
trailingWhitespaceLength += clusterLength;
glyphCount++;
}
}
}
}
@ -753,67 +805,73 @@ namespace Avalonia.Media
newLineLength = 0;
var trailingWhitespaceLength = 0;
if (GlyphClusters == null)
if (Characters != null)
{
for (var i = 0; i < Characters.Length;)
if (GlyphClusters == null)
{
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
if (!codepoint.IsWhiteSpace)
for (var i = 0; i < Characters.Count;)
{
break;
}
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
if (codepoint.IsBreakChar)
{
newLineLength++;
}
if (!codepoint.IsWhiteSpace)
{
break;
}
trailingWhitespaceLength++;
if (codepoint.IsBreakChar)
{
newLineLength++;
}
i += count;
glyphCount++;
trailingWhitespaceLength++;
i += count;
glyphCount++;
}
}
}
else
{
for (var i = 0; i < GlyphClusters.Count; i++)
else
{
var currentCluster = GlyphClusters[i];
var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
var characterIndex = 0;
if (!codepoint.IsWhiteSpace)
for (var i = 0; i < GlyphClusters.Count; i++)
{
break;
}
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength);
var clusterLength = 1;
characterIndex += characterLength;
var j = i;
if (!codepoint.IsWhiteSpace)
{
break;
}
while (j - 1 >= 0)
{
var nextCluster = GlyphClusters[--j];
var clusterLength = 1;
if (currentCluster == nextCluster)
var j = i;
while (j - 1 >= 0)
{
clusterLength++;
var nextCluster = GlyphClusters[--j];
continue;
}
if (currentCluster == nextCluster)
{
clusterLength++;
break;
}
continue;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
break;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
trailingWhitespaceLength += clusterLength;
trailingWhitespaceLength += clusterLength;
glyphCount += clusterLength;
glyphCount += clusterLength;
}
}
}
@ -855,14 +913,9 @@ namespace Avalonia.Media
throw new InvalidOperationException();
}
_glyphRunImpl = CreateGlyphRunImpl();
}
private IGlyphRunImpl CreateGlyphRunImpl()
{
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
_glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
}
void IDisposable.Dispose()

18
src/Avalonia.Base/Media/GlyphRunMetrics.cs

@ -2,24 +2,30 @@
{
public readonly struct GlyphRunMetrics
{
public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, int trailingWhitespaceLength,
int newlineLength, double height)
public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, double height,
int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster)
{
Width = width;
WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
TrailingWhitespaceLength = trailingWhitespaceLength;
NewlineLength = newlineLength;
Height = height;
TrailingWhitespaceLength = trailingWhitespaceLength;
NewLineLength= newLineLength;
FirstCluster = firstCluster;
LastCluster = lastCluster;
}
public double Width { get; }
public double WidthIncludingTrailingWhitespace { get; }
public double Height { get; }
public int TrailingWhitespaceLength { get; }
public int NewlineLength { get; }
public int NewLineLength { get; }
public double Height { get; }
public int FirstCluster { get; }
public int LastCluster { get; }
}
}

293
src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs

@ -0,0 +1,293 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
public readonly struct CharacterBufferRange : IReadOnlyList<char>
{
/// <summary>
/// Getting an empty character string
/// </summary>
public static CharacterBufferRange Empty => new CharacterBufferRange();
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from character array
/// </summary>
/// <param name="characterArray">character array</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
/// <param name="characterLength">character length</param>
public CharacterBufferRange(
char[] characterArray,
int offsetToFirstChar,
int characterLength
)
: this(
new CharacterBufferReference(characterArray, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from string
/// </summary>
/// <param name="characterString">character string</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
/// <param name="characterLength">character length</param>
public CharacterBufferRange(
string characterString,
int offsetToFirstChar,
int characterLength
)
: this(
new CharacterBufferReference(characterString, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from <see cref="CharacterBufferReference"/>
/// </summary>
/// <param name="characterBufferReference">character buffer reference</param>
/// <param name="characterLength">number of characters</param>
public CharacterBufferRange(
CharacterBufferReference characterBufferReference,
int characterLength
)
{
if (characterLength < 0)
{
throw new ArgumentOutOfRangeException("characterLength", "ParameterCannotBeNegative");
}
int maxLength = characterBufferReference.CharacterBuffer.Length > 0 ?
characterBufferReference.CharacterBuffer.Length - characterBufferReference.OffsetToFirstChar :
0;
if (characterLength > maxLength)
{
throw new ArgumentOutOfRangeException("characterLength", $"ParameterCannotBeGreaterThan {maxLength}");
}
CharacterBufferReference = characterBufferReference;
Length = characterLength;
}
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from part of another <see cref="CharacterBufferRange"/>
/// </summary>
internal CharacterBufferRange(
CharacterBufferRange characterBufferRange,
int offsetToFirstChar,
int characterLength
) :
this(
characterBufferRange.CharacterBuffer,
characterBufferRange.OffsetToFirstChar + offsetToFirstChar,
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from string
/// </summary>
internal CharacterBufferRange(
string charString
) :
this(
charString,
0,
charString.Length
)
{ }
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from memory buffer
/// </summary>
internal CharacterBufferRange(
ReadOnlyMemory<char> charBuffer,
int offsetToFirstChar,
int characterLength
) :
this(
new CharacterBufferReference(charBuffer, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> by extracting text info from a text run
/// </summary>
internal CharacterBufferRange(TextRun textRun)
{
CharacterBufferReference = textRun.CharacterBufferReference;
Length = textRun.Length;
}
public char this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
#if DEBUG
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
#endif
return Span[index];
}
}
/// <summary>
/// Gets a reference to the character buffer
/// </summary>
public CharacterBufferReference CharacterBufferReference { get; }
/// <summary>
/// Gets the number of characters in text source character store
/// </summary>
public int Length { get; }
/// <summary>
/// Gets a span from the character buffer range
/// </summary>
public ReadOnlySpan<char> Span =>
CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length);
/// <summary>
/// Gets the character memory buffer
/// </summary>
internal ReadOnlyMemory<char> CharacterBuffer
{
get { return CharacterBufferReference.CharacterBuffer; }
}
/// <summary>
/// Gets the character offset relative to the beginning of buffer to
/// the first character of the run
/// </summary>
internal int OffsetToFirstChar
{
get { return CharacterBufferReference.OffsetToFirstChar; }
}
/// <summary>
/// Indicate whether the character buffer range is empty
/// </summary>
internal bool IsEmpty
{
get { return CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0; }
}
internal CharacterBufferRange Take(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new CharacterBufferRange(CharacterBufferReference, length);
}
internal CharacterBufferRange Skip(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
if (length == Length)
{
return new CharacterBufferRange(new CharacterBufferReference(), 0);
}
var characterBufferReference = new CharacterBufferReference(
CharacterBufferReference.CharacterBuffer,
CharacterBufferReference.OffsetToFirstChar + length);
return new CharacterBufferRange(characterBufferReference, Length - length);
}
/// <summary>
/// Compute hash code
/// </summary>
public override int GetHashCode()
{
return CharacterBufferReference.GetHashCode() ^ Length;
}
/// <summary>
/// Test equality with the input object
/// </summary>
/// <param name="obj"> The object to test </param>
public override bool Equals(object? obj)
{
if (obj is CharacterBufferRange range)
{
return Equals(range);
}
return false;
}
/// <summary>
/// Test equality with the input CharacterBufferRange
/// </summary>
/// <param name="value"> The CharacterBufferRange value to test </param>
public bool Equals(CharacterBufferRange value)
{
return CharacterBufferReference.Equals(value.CharacterBufferReference)
&& Length == value.Length;
}
/// <summary>
/// Compare two CharacterBufferRange for equality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator ==(CharacterBufferRange left, CharacterBufferRange right)
{
return left.Equals(right);
}
/// <summary>
/// Compare two CharacterBufferRange for inequality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator !=(CharacterBufferRange left, CharacterBufferRange right)
{
return !(left == right);
}
int IReadOnlyCollection<char>.Count => Length;
public IEnumerator<char> GetEnumerator()
{
return new ImmutableReadOnlyListStructEnumerator<char>(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

115
src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs

@ -0,0 +1,115 @@
using System;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Text character buffer reference
/// </summary>
public readonly struct CharacterBufferReference : IEquatable<CharacterBufferReference>
{
/// <summary>
/// Construct character buffer reference from character array
/// </summary>
/// <param name="characterArray">character array</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
public CharacterBufferReference(char[] characterArray, int offsetToFirstChar = 0)
: this(characterArray.AsMemory(), offsetToFirstChar)
{ }
/// <summary>
/// Construct character buffer reference from string
/// </summary>
/// <param name="characterString">character string</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
public CharacterBufferReference(string characterString, int offsetToFirstChar = 0)
: this(characterString.AsMemory(), offsetToFirstChar)
{ }
/// <summary>
/// Construct character buffer reference from memory buffer
/// </summary>
internal CharacterBufferReference(ReadOnlyMemory<char> characterBuffer, int offsetToFirstChar = 0)
{
if (offsetToFirstChar < 0)
{
throw new ArgumentOutOfRangeException("offsetToFirstChar", "ParameterCannotBeNegative");
}
// maximum offset is one less than CharacterBuffer.Count, except that zero is always a valid offset
// even in the case of an empty or null character buffer
var maxOffset = characterBuffer.Length == 0 ? 0 : Math.Max(0, characterBuffer.Length - 1);
if (offsetToFirstChar > maxOffset)
{
throw new ArgumentOutOfRangeException("offsetToFirstChar", $"ParameterCannotBeGreaterThan, {maxOffset}");
}
CharacterBuffer = characterBuffer;
OffsetToFirstChar = offsetToFirstChar;
}
/// <summary>
/// Gets the character memory buffer
/// </summary>
public ReadOnlyMemory<char> CharacterBuffer { get; }
/// <summary>
/// Gets the character offset relative to the beginning of buffer to
/// the first character of the run
/// </summary>
public int OffsetToFirstChar { get; }
/// <summary>
/// Compute hash code
/// </summary>
public override int GetHashCode()
{
return CharacterBuffer.IsEmpty ? 0 : CharacterBuffer.GetHashCode();
}
/// <summary>
/// Test equality with the input object
/// </summary>
/// <param name="obj"> The object to test. </param>
public override bool Equals(object? obj)
{
if (obj is CharacterBufferReference reference)
{
return Equals(reference);
}
return false;
}
/// <summary>
/// Test equality with the input CharacterBufferReference
/// </summary>
/// <param name="value"> The characterBufferReference value to test </param>
public bool Equals(CharacterBufferReference value)
{
return CharacterBuffer.Equals(value.CharacterBuffer);
}
/// <summary>
/// Compare two CharacterBufferReference for equality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator ==(CharacterBufferReference left, CharacterBufferReference right)
{
return left.Equals(right);
}
/// <summary>
/// Compare two CharacterBufferReference for inequality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator !=(CharacterBufferReference left, CharacterBufferReference right)
{
return !(left == right);
}
}
}

13
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

@ -7,14 +7,15 @@ namespace Avalonia.Media.TextFormatting
{
internal readonly struct FormattedTextSource : ITextSource
{
private readonly ReadOnlySlice<char> _text;
private readonly CharacterBufferRange _text;
private readonly int length;
private readonly TextRunProperties _defaultProperties;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textModifier;
public FormattedTextSource(ReadOnlySlice<char> text, TextRunProperties defaultProperties,
public FormattedTextSource(string text, TextRunProperties defaultProperties,
IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{
_text = text;
_text = new CharacterBufferRange(text);
_defaultProperties = defaultProperties;
_textModifier = textModifier;
}
@ -35,7 +36,7 @@ namespace Avalonia.Media.TextFormatting
var textStyleRun = CreateTextStyleRun(runText, textSourceIndex, _defaultProperties, _textModifier);
return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value);
return new TextCharacters(runText.Take(textStyleRun.Length).CharacterBufferReference, textStyleRun.Length, textStyleRun.Value);
}
/// <summary>
@ -48,7 +49,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns>
/// The created text style run.
/// </returns>
private static ValueSpan<TextRunProperties> CreateTextStyleRun(ReadOnlySlice<char> text, int firstTextSourceIndex,
private static ValueSpan<TextRunProperties> CreateTextStyleRun(CharacterBufferRange text, int firstTextSourceIndex,
TextRunProperties defaultProperties, IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{
if (textModifier == null || textModifier.Count == 0)
@ -122,7 +123,7 @@ namespace Avalonia.Media.TextFormatting
return new ValueSpan<TextRunProperties>(firstTextSourceIndex, length, currentProperties);
}
private static int CoerceLength(ReadOnlySlice<char> text, int length)
private static int CoerceLength(CharacterBufferRange text, int length)
{
var finalLength = 0;

19
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@ -46,28 +46,30 @@ namespace Avalonia.Media.TextFormatting
var breakOportunities = new Queue<int>();
var currentPosition = textLine.FirstTextSourceIndex;
foreach (var textRun in lineImpl.TextRuns)
{
var text = textRun.Text;
var text = new CharacterBufferRange(textRun);
if (text.IsEmpty)
{
continue;
}
var start = text.Start;
var lineBreakEnumerator = new LineBreakEnumerator(text);
while (lineBreakEnumerator.MoveNext())
{
var currentBreak = lineBreakEnumerator.Current;
if (!currentBreak.Required && currentBreak.PositionWrap != text.Length)
if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length)
{
breakOportunities.Enqueue(start + currentBreak.PositionMeasure);
breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure);
}
}
currentPosition += textRun.Length;
}
if (breakOportunities.Count == 0)
@ -78,9 +80,11 @@ namespace Avalonia.Media.TextFormatting
var remainingSpace = Math.Max(0, paragraphWidth - lineImpl.WidthIncludingTrailingWhitespace);
var spacing = remainingSpace / breakOportunities.Count;
currentPosition = textLine.FirstTextSourceIndex;
foreach (var textRun in lineImpl.TextRuns)
{
var text = textRun.Text;
var text = textRun.CharacterBufferReference.CharacterBuffer;
if (text.IsEmpty)
{
@ -91,7 +95,6 @@ namespace Avalonia.Media.TextFormatting
{
var glyphRun = shapedText.GlyphRun;
var shapedBuffer = shapedText.ShapedBuffer;
var currentPosition = text.Start;
while (breakOportunities.Count > 0)
{
@ -110,6 +113,8 @@ namespace Avalonia.Media.TextFormatting
glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances;
}
currentPosition += textRun.Length;
}
}
}

20
src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs

@ -7,30 +7,26 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public sealed class ShapeableTextCharacters : TextRun
{
public ShapeableTextCharacters(ReadOnlySlice<char> text, TextRunProperties properties, sbyte biDiLevel)
public ShapeableTextCharacters(CharacterBufferReference characterBufferReference, int length,
TextRunProperties properties, sbyte biDiLevel)
{
TextSourceLength = text.Length;
Text = text;
CharacterBufferReference = characterBufferReference;
Length = length;
Properties = properties;
BidiLevel = biDiLevel;
}
public override int TextSourceLength { get; }
public override int Length { get; }
public override ReadOnlySlice<char> Text { get; }
public override CharacterBufferReference CharacterBufferReference { get; }
public override TextRunProperties Properties { get; }
public sbyte BidiLevel { get; }
public bool CanShapeTogether(ShapeableTextCharacters shapeableTextCharacters)
{
if (!Text.Buffer.Equals(shapeableTextCharacters.Text.Buffer))
{
return false;
}
if (Text.Start + Text.Length != shapeableTextCharacters.Text.Start)
if (!CharacterBufferReference.Equals(shapeableTextCharacters.CharacterBufferReference))
{
return false;
}

31
src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs

@ -7,16 +7,16 @@ namespace Avalonia.Media.TextFormatting
public sealed class ShapedBuffer : IList<GlyphInfo>
{
private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters();
public ShapedBuffer(ReadOnlySlice<char> text, int length, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
: this(text, new GlyphInfo[length], glyphTypeface, fontRenderingEmSize, bidiLevel)
public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) :
this(characterBufferRange, new GlyphInfo[bufferLength], glyphTypeface, fontRenderingEmSize, bidiLevel)
{
}
internal ShapedBuffer(ReadOnlySlice<char> text, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
internal ShapedBuffer(CharacterBufferRange characterBufferRange, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
{
Text = text;
CharacterBufferRange = characterBufferRange;
GlyphInfos = glyphInfos;
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
@ -24,9 +24,7 @@ namespace Avalonia.Media.TextFormatting
}
internal ArraySlice<GlyphInfo> GlyphInfos { get; }
public ReadOnlySlice<char> Text { get; }
public int Length => GlyphInfos.Length;
public IGlyphTypeface GlyphTypeface { get; }
@ -45,6 +43,8 @@ namespace Avalonia.Media.TextFormatting
public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos);
public CharacterBufferRange CharacterBufferRange { get; }
/// <summary>
/// Finds a glyph index for given character index.
/// </summary>
@ -105,16 +105,23 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The split result.</returns>
internal SplitResult<ShapedBuffer> Split(int length)
{
if (Text.Length == length)
if (CharacterBufferRange.Length == length)
{
return new SplitResult<ShapedBuffer>(this, null);
}
var glyphCount = FindGlyphIndex(Text.Start + length);
var firstCluster = GlyphClusters[0];
var lastCluster = GlyphClusters[GlyphClusters.Count - 1];
var start = firstCluster < lastCluster ? firstCluster : lastCluster;
var glyphCount = FindGlyphIndex(start + length);
var first = new ShapedBuffer(Text.Take(length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
var first = new ShapedBuffer(CharacterBufferRange.Take(length),
GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
var second = new ShapedBuffer(Text.Skip(length), GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
var second = new ShapedBuffer(CharacterBufferRange.Skip(length),
GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
return new SplitResult<ShapedBuffer>(first, second);
}

19
src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs

@ -1,6 +1,5 @@
using System;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -14,10 +13,10 @@ namespace Avalonia.Media.TextFormatting
public ShapedTextCharacters(ShapedBuffer shapedBuffer, TextRunProperties properties)
{
ShapedBuffer = shapedBuffer;
Text = shapedBuffer.Text;
CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference;
Length = shapedBuffer.CharacterBufferRange.Length;
Properties = properties;
TextSourceLength = Text.Length;
TextMetrics = new TextMetrics(properties.Typeface, properties.FontRenderingEmSize);
TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize);
}
public bool IsReversed { get; private set; }
@ -27,13 +26,13 @@ namespace Avalonia.Media.TextFormatting
public ShapedBuffer ShapedBuffer { get; }
/// <inheritdoc/>
public override ReadOnlySlice<char> Text { get; }
public override CharacterBufferReference CharacterBufferReference { get; }
/// <inheritdoc/>
public override TextRunProperties Properties { get; }
/// <inheritdoc/>
public override int TextSourceLength { get; }
public override int Length { get; }
public TextMetrics TextMetrics { get; }
@ -176,12 +175,12 @@ namespace Avalonia.Media.TextFormatting
#if DEBUG
if (first.Text.Length != length)
if (first.Length != length)
{
throw new InvalidOperationException("Split length mismatch.");
}
#endif
#endif
var second = new ShapedTextCharacters(splitBuffer.Second!, Properties);
@ -193,7 +192,7 @@ namespace Avalonia.Media.TextFormatting
return new GlyphRun(
ShapedBuffer.GlyphTypeface,
ShapedBuffer.FontRenderingEmSize,
Text,
new CharacterBufferRange(CharacterBufferReference, Length),
ShapedBuffer.GlyphIndices,
ShapedBuffer.GlyphAdvances,
ShapedBuffer.GlyphOffsets,

2
src/Avalonia.Base/Media/TextFormatting/SplitResult.cs

@ -1,6 +1,6 @@
namespace Avalonia.Media.TextFormatting
{
internal readonly struct SplitResult<T>
public readonly struct SplitResult<T>
{
public SplitResult(T first, T? second)
{

129
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -10,26 +9,83 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public class TextCharacters : TextRun
{
public TextCharacters(ReadOnlySlice<char> text, TextRunProperties properties)
{
TextSourceLength = text.Length;
Text = text;
Properties = properties;
}
/// <summary>
/// Construct a run of text content from character array
/// </summary>
public TextCharacters(
char[] characterArray,
int offsetToFirstChar,
int length,
TextRunProperties textRunProperties
) :
this(
new CharacterBufferReference(characterArray, offsetToFirstChar),
length,
textRunProperties
)
{ }
public TextCharacters(ReadOnlySlice<char> text, int offsetToFirstCharacter, int length,
TextRunProperties properties)
/// <summary>
/// Construct a run for text content from string
/// </summary>
public TextCharacters(
string characterString,
TextRunProperties textRunProperties
) :
this(
characterString,
0, // offsetToFirstChar
(characterString == null) ? 0 : characterString.Length,
textRunProperties
)
{ }
/// <summary>
/// Construct a run for text content from string
/// </summary>
public TextCharacters(
string characterString,
int offsetToFirstChar,
int length,
TextRunProperties textRunProperties
) :
this(
new CharacterBufferReference(characterString, offsetToFirstChar),
length,
textRunProperties
)
{ }
/// <summary>
/// Internal constructor of TextContent
/// </summary>
public TextCharacters(
CharacterBufferReference characterBufferReference,
int length,
TextRunProperties textRunProperties
)
{
Text = text.Skip(offsetToFirstCharacter).Take(length);
TextSourceLength = length;
Properties = properties;
if (length <= 0)
{
throw new ArgumentOutOfRangeException("length", "ParameterMustBeGreaterThanZero");
}
if (textRunProperties.FontRenderingEmSize <= 0)
{
throw new ArgumentOutOfRangeException("textRunProperties.FontRenderingEmSize", "ParameterMustBeGreaterThanZero");
}
CharacterBufferReference = characterBufferReference;
Length = length;
Properties = textRunProperties;
}
/// <inheritdoc />
public override int TextSourceLength { get; }
public override int Length { get; }
/// <inheritdoc />
public override ReadOnlySlice<char> Text { get; }
public override CharacterBufferReference CharacterBufferReference { get; }
/// <inheritdoc />
public override TextRunProperties Properties { get; }
@ -38,18 +94,17 @@ namespace Avalonia.Media.TextFormatting
/// Gets a list of <see cref="ShapeableTextCharacters"/>.
/// </summary>
/// <returns>The shapeable text characters.</returns>
internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(ReadOnlySlice<char> runText, sbyte biDiLevel,
ref TextRunProperties? previousProperties)
internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties)
{
var shapeableCharacters = new List<ShapeableTextCharacters>(2);
while (!runText.IsEmpty)
while (characterBufferRange.Length > 0)
{
var shapeableRun = CreateShapeableRun(runText, Properties, biDiLevel, ref previousProperties);
var shapeableRun = CreateShapeableRun(characterBufferRange, Properties, biDiLevel, ref previousProperties);
shapeableCharacters.Add(shapeableRun);
runText = runText.Skip(shapeableRun.Text.Length);
characterBufferRange = characterBufferRange.Skip(shapeableRun.Length);
previousProperties = shapeableRun.Properties;
}
@ -60,45 +115,45 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Creates a shapeable text run with unique properties.
/// </summary>
/// <param name="text">The text to create text runs from.</param>
/// <param name="characterBufferRange">The character buffer range 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 static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice<char> text,
private static ShapeableTextCharacters CreateShapeableRun(CharacterBufferRange characterBufferRange,
TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
{
var defaultTypeface = defaultProperties.Typeface;
var currentTypeface = defaultTypeface;
var previousTypeface = previousProperties?.Typeface;
if (TryGetShapeableLength(text, currentTypeface, null, out var count, out var script))
if (TryGetShapeableLength(characterBufferRange, currentTypeface, null, out var count, out var script))
{
if (script == Script.Common && previousTypeface is not null)
{
if (TryGetShapeableLength(text, previousTypeface.Value, null, out var fallbackCount, out _))
if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _))
{
return new ShapeableTextCharacters(text.Take(fallbackCount),
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, fallbackCount,
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
}
}
return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
if (previousTypeface is not null)
{
if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _))
if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _))
{
return new ShapeableTextCharacters(text.Take(count),
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count,
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
}
}
var codepoint = Codepoint.ReplacementCodepoint;
var codepointEnumerator = new CodepointEnumerator(text.Skip(count));
var codepointEnumerator = new CodepointEnumerator(characterBufferRange.Skip(count));
while (codepointEnumerator.MoveNext())
{
@ -118,10 +173,10 @@ namespace Avalonia.Media.TextFormatting
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
out currentTypeface);
if (matchFound && TryGetShapeableLength(text, currentTypeface, defaultTypeface, out count, out _))
if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _))
{
//Fallback found
return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
@ -130,7 +185,7 @@ namespace Avalonia.Media.TextFormatting
var glyphTypeface = currentTypeface.GlyphTypeface;
var enumerator = new GraphemeEnumerator(text);
var enumerator = new GraphemeEnumerator(characterBufferRange);
while (enumerator.MoveNext())
{
@ -144,20 +199,20 @@ namespace Avalonia.Media.TextFormatting
count += grapheme.Text.Length;
}
return new ShapeableTextCharacters(text.Take(count), defaultProperties, biDiLevel);
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel);
}
/// <summary>
/// Tries to get a shapeable length that is supported by the specified typeface.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="characterBufferRange">The character buffer range to shape.</param>
/// <param name="typeface">The typeface that is used to find matching characters.</param>
/// <param name="defaultTypeface"></param>
/// <param name="length">The shapeable length.</param>
/// <param name="script"></param>
/// <returns></returns>
protected static bool TryGetShapeableLength(
ReadOnlySlice<char> text,
internal static bool TryGetShapeableLength(
CharacterBufferRange characterBufferRange,
Typeface typeface,
Typeface? defaultTypeface,
out int length,
@ -166,7 +221,7 @@ namespace Avalonia.Media.TextFormatting
length = 0;
script = Script.Unknown;
if (text.Length == 0)
if (characterBufferRange.Length == 0)
{
return false;
}
@ -174,7 +229,7 @@ namespace Avalonia.Media.TextFormatting
var font = typeface.GlyphTypeface;
var defaultFont = defaultTypeface?.GlyphTypeface;
var enumerator = new GraphemeEnumerator(text);
var enumerator = new GraphemeEnumerator(characterBufferRange);
while (enumerator.MoveNext())
{

102
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@ -32,86 +32,88 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case ShapedTextCharacters shapedRun:
{
currentWidth += shapedRun.Size.Width;
if (currentWidth > availableWidth)
{
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
currentWidth += shapedRun.Size.Width;
if (currentWidth > availableWidth)
{
if (isWordEllipsis && measuredLength < textLine.Length)
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{
var currentBreakPosition = 0;
if (isWordEllipsis && measuredLength < textLine.Length)
{
var currentBreakPosition = 0;
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
var text = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
var nextBreakPosition = lineBreaker.Current.PositionMeasure;
var lineBreaker = new LineBreakEnumerator(text);
if (nextBreakPosition == 0)
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
break;
}
var nextBreakPosition = lineBreaker.Current.PositionMeasure;
if (nextBreakPosition >= measuredLength)
{
break;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition >= measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
currentBreakPosition = nextBreakPosition;
measuredLength = currentBreakPosition;
}
measuredLength = currentBreakPosition;
}
}
collapsedLength += measuredLength;
collapsedLength += measuredLength;
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
collapsedRuns.AddRange(splitResult.First);
}
collapsedRuns.Add(shapedSymbol);
collapsedRuns.Add(shapedSymbol);
return collapsedRuns;
}
return collapsedRuns;
}
availableWidth -= currentRun.Size.Width;
availableWidth -= currentRun.Size.Width;
break;
}
break;
}
case { } drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
if (collapsedLength > 0)
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
collapsedRuns.AddRange(splitResult.First);
}
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
collapsedRuns.Add(shapedSymbol);
collapsedRuns.Add(shapedSymbol);
return collapsedRuns;
}
return collapsedRuns;
break;
}
break;
}
}
collapsedLength += currentRun.TextSourceLength;
collapsedLength += currentRun.Length;
runIndex++;
}

4
src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs

@ -7,9 +7,9 @@
{
public TextEndOfLine(int textSourceLength = DefaultTextSourceLength)
{
TextSourceLength = textSourceLength;
Length = textSourceLength;
}
public override int TextSourceLength { get; }
public override int Length { get; }
}
}

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

@ -79,14 +79,14 @@ namespace Avalonia.Media.TextFormatting
{
var currentRun = textRuns[i];
if (currentLength + currentRun.TextSourceLength < length)
if (currentLength + currentRun.Length < length)
{
currentLength += currentRun.TextSourceLength;
currentLength += currentRun.Length;
continue;
}
var firstCount = currentRun.TextSourceLength >= 1 ? i + 1 : i;
var firstCount = currentRun.Length >= 1 ? i + 1 : i;
var first = new List<DrawableTextRun>(firstCount);
@ -100,13 +100,13 @@ namespace Avalonia.Media.TextFormatting
var secondCount = textRuns.Count - firstCount;
if (currentLength + currentRun.TextSourceLength == length)
if (currentLength + currentRun.Length == length)
{
var second = secondCount > 0 ? new List<DrawableTextRun>(secondCount) : null;
if (second != null)
{
var offset = currentRun.TextSourceLength >= 1 ? 1 : 0;
var offset = currentRun.Length >= 1 ? 1 : 0;
for (var j = 0; j < secondCount; j++)
{
@ -163,15 +163,17 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in textRuns)
{
if (textRun.Text.IsEmpty)
if (textRun.CharacterBufferReference.CharacterBuffer.Length == 0)
{
var text = new char[textRun.TextSourceLength];
var characterBuffer = new CharacterBufferReference(new char[textRun.Length]);
biDiData.Append(text);
biDiData.Append(new CharacterBufferRange(characterBuffer, textRun.Length));
}
else
{
biDiData.Append(textRun.Text);
var text = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
biDiData.Append(text);
}
}
@ -207,10 +209,9 @@ namespace Avalonia.Media.TextFormatting
case ShapeableTextCharacters shapeableRun:
{
var groupedRuns = new List<ShapeableTextCharacters>(2) { shapeableRun };
var text = currentRun.Text;
var start = currentRun.Text.Start;
var length = currentRun.Text.Length;
var bufferOffset = currentRun.Text.BufferOffset;
var characterBufferReference = currentRun.CharacterBufferReference;
var length = currentRun.Length;
var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar;
while (index + 1 < processedRuns.Count)
{
@ -223,19 +224,14 @@ namespace Avalonia.Media.TextFormatting
{
groupedRuns.Add(nextRun);
length += nextRun.Text.Length;
if (start > nextRun.Text.Start)
{
start = nextRun.Text.Start;
}
length += nextRun.Length;
if (bufferOffset > nextRun.Text.BufferOffset)
if (offsetToFirstCharacter > nextRun.CharacterBufferReference.OffsetToFirstChar)
{
bufferOffset = nextRun.Text.BufferOffset;
offsetToFirstCharacter = nextRun.CharacterBufferReference.OffsetToFirstChar;
}
text = new ReadOnlySlice<char>(text.Buffer, start, length, bufferOffset);
characterBufferReference = new CharacterBufferReference(characterBufferReference.CharacterBuffer, offsetToFirstCharacter);
index++;
@ -252,7 +248,7 @@ namespace Avalonia.Media.TextFormatting
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
drawableTextRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions));
break;
}
@ -263,17 +259,17 @@ namespace Avalonia.Media.TextFormatting
}
private static IReadOnlyList<ShapedTextCharacters> ShapeTogether(
IReadOnlyList<ShapeableTextCharacters> textRuns, ReadOnlySlice<char> text, TextShaperOptions options)
IReadOnlyList<ShapeableTextCharacters> textRuns, CharacterBufferReference text, int length, TextShaperOptions options)
{
var shapedRuns = new List<ShapedTextCharacters>(textRuns.Count);
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
var shapedBuffer = TextShaper.Current.ShapeText(text, length, options);
for (var i = 0; i < textRuns.Count; i++)
{
var currentRun = textRuns[i];
var splitResult = shapedBuffer.Split(currentRun.Text.Length);
var splitResult = shapedBuffer.Split(currentRun.Length);
shapedRuns.Add(new ShapedTextCharacters(splitResult.First, currentRun.Properties));
@ -301,7 +297,7 @@ namespace Avalonia.Media.TextFormatting
TextRunProperties? previousProperties = null;
TextCharacters? currentRun = null;
var runText = ReadOnlySlice<char>.Empty;
CharacterBufferRange runText = default;
for (var i = 0; i < textCharacters.Count; i++)
{
@ -314,12 +310,12 @@ namespace Avalonia.Media.TextFormatting
yield return new[] { drawableRun };
levelIndex += drawableRun.TextSourceLength;
levelIndex += drawableRun.Length;
continue;
}
runText = currentRun.Text;
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
for (; j < runText.Length;)
{
@ -401,7 +397,7 @@ namespace Avalonia.Media.TextFormatting
{
endOfLine = textEndOfLine;
textSourceLength += textEndOfLine.TextSourceLength;
textSourceLength += textEndOfLine.Length;
textRuns.Add(textRun);
@ -414,7 +410,7 @@ namespace Avalonia.Media.TextFormatting
{
if (TryGetLineBreak(textCharacters, out var runLineBreak))
{
var splitResult = new TextCharacters(textCharacters.Text.Take(runLineBreak.PositionWrap),
var splitResult = new TextCharacters(textCharacters.CharacterBufferReference, runLineBreak.PositionWrap,
textCharacters.Properties);
textRuns.Add(splitResult);
@ -435,7 +431,7 @@ namespace Avalonia.Media.TextFormatting
}
}
textSourceLength += textRun.TextSourceLength;
textSourceLength += textRun.Length;
}
return textRuns;
@ -445,12 +441,14 @@ namespace Avalonia.Media.TextFormatting
{
lineBreak = default;
if (textRun.Text.IsEmpty)
if (textRun.CharacterBufferReference.CharacterBuffer.IsEmpty)
{
return false;
}
var lineBreakEnumerator = new LineBreakEnumerator(textRun.Text);
var characterBufferRange = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
var lineBreakEnumerator = new LineBreakEnumerator(characterBufferRange);
while (lineBreakEnumerator.MoveNext())
{
@ -461,7 +459,7 @@ namespace Avalonia.Media.TextFormatting
lineBreak = lineBreakEnumerator.Current;
return lineBreak.PositionWrap >= textRun.Text.Length || true;
return lineBreak.PositionWrap >= textRun.Length || true;
}
return false;
@ -480,7 +478,7 @@ namespace Avalonia.Media.TextFormatting
{
if(shapedTextCharacters.ShapedBuffer.Length > 0)
{
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0];
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster;
var lastCluster = firstCluster;
for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
@ -498,7 +496,7 @@ namespace Avalonia.Media.TextFormatting
currentWidth += glyphInfo.GlyphAdvance;
}
measuredLength += currentRun.TextSourceLength;
measuredLength += currentRun.Length;
}
break;
@ -511,7 +509,7 @@ namespace Avalonia.Media.TextFormatting
goto found;
}
measuredLength += currentRun.TextSourceLength;
measuredLength += currentRun.Length;
currentWidth += currentRun.Size.Width;
break;
@ -533,11 +531,11 @@ namespace Avalonia.Media.TextFormatting
var flowDirection = paragraphProperties.FlowDirection;
var properties = paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.Typeface.GlyphTypeface;
var text = new ReadOnlySlice<char>(s_empty, firstTextSourceIndex, 1);
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
var characterBufferRange = new CharacterBufferRange(new CharacterBufferReference(s_empty), s_empty.Length);
var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection);
var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
@ -579,7 +577,9 @@ namespace Avalonia.Media.TextFormatting
{
var currentRun = textRuns[index];
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
var lineBreaker = new LineBreakEnumerator(runText);
var breakFound = false;
@ -612,7 +612,7 @@ namespace Avalonia.Media.TextFormatting
//Find next possible wrap position (overflow)
if (index < textRuns.Count - 1)
{
if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
if (lineBreaker.Current.PositionWrap != currentRun.Length)
{
//We already found the next possible wrap position.
breakFound = true;
@ -626,7 +626,7 @@ namespace Avalonia.Media.TextFormatting
{
currentPosition += lineBreaker.Current.PositionWrap;
if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
if (lineBreaker.Current.PositionWrap != currentRun.Length)
{
break;
}
@ -640,7 +640,9 @@ namespace Avalonia.Media.TextFormatting
currentRun = textRuns[index];
lineBreaker = new LineBreakEnumerator(currentRun.Text);
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
lineBreaker = new LineBreakEnumerator(runText);
}
}
else
@ -669,7 +671,7 @@ namespace Avalonia.Media.TextFormatting
if (!breakFound)
{
currentLength += currentRun.TextSourceLength;
currentLength += currentRun.Length;
continue;
}
@ -723,12 +725,12 @@ namespace Avalonia.Media.TextFormatting
return false;
}
if (Current.TextSourceLength == 0)
if (Current.Length == 0)
{
return false;
}
_pos += Current.TextSourceLength;
_pos += Current.Length;
return true;
}
@ -754,7 +756,9 @@ namespace Avalonia.Media.TextFormatting
var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo);
var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions);
var characterBuffer = textRun.CharacterBufferReference;
var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions);
return new ShapedTextCharacters(shapedBuffer, textRun.Properties);
}

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

@ -55,7 +55,7 @@ namespace Avalonia.Media.TextFormatting
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
textDecorations, flowDirection, lineHeight, letterSpacing);
_textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
_textSource = new FormattedTextSource(text ?? "", _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
_textTrimming = textTrimming ?? TextTrimming.None;

5
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -19,7 +18,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="width">width in which collapsing is constrained to</param>
/// <param name="textRunProperties">text run properties of ellipsis symbol</param>
public TextLeadingPrefixCharacterEllipsis(
ReadOnlySlice<char> ellipsis,
string ellipsis,
int prefixLength,
double width,
TextRunProperties textRunProperties)
@ -129,7 +128,7 @@ namespace Avalonia.Media.TextFormatting
if (suffixCount > 0)
{
var splitSuffix =
endShapedRun.Split(run.TextSourceLength - suffixCount);
endShapedRun.Split(run.Length - suffixCount);
collapsedRuns.Add(splitSuffix.Second!);
}

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

@ -56,7 +56,7 @@ namespace Avalonia.Media.TextFormatting
public override double Height => _textLineMetrics.Height;
/// <inheritdoc/>
public override int NewLineLength => _textLineMetrics.NewLineLength;
public override int NewLineLength => _textLineMetrics.NewlineLength;
/// <inheritdoc/>
public override double OverhangAfter => 0;
@ -180,7 +180,7 @@ namespace Avalonia.Media.TextFormatting
{
var lastRun = _textRuns[_textRuns.Count - 1];
return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.TextSourceLength, lastRun.Size.Width);
return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, lastRun.Size.Width);
}
// process hit that happens within the line
@ -195,18 +195,18 @@ namespace Avalonia.Media.TextFormatting
if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
{
var rightToLeftIndex = i;
currentPosition += currentRun.TextSourceLength;
currentPosition += currentRun.Length;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
{
var nextShaped = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters;
var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextCharacters;
if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight)
{
break;
}
currentPosition += nextShaped.TextSourceLength;
currentPosition += nextShaped.Length;
rightToLeftIndex++;
}
@ -223,27 +223,26 @@ namespace Avalonia.Media.TextFormatting
if (currentDistance + currentRun.Size.Width <= distance)
{
currentDistance += currentRun.Size.Width;
currentPosition -= currentRun.TextSourceLength;
currentPosition -= currentRun.Length;
continue;
}
characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
break;
return GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
}
}
if (currentDistance + currentRun.Size.Width < distance)
characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
if (i < _textRuns.Count - 1 && currentDistance + currentRun.Size.Width < distance)
{
currentDistance += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
currentPosition += currentRun.Length;
continue;
}
characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
break;
}
@ -264,10 +263,10 @@ namespace Avalonia.Media.TextFormatting
if (shapedRun.GlyphRun.IsLeftToRight)
{
offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster);
}
characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
characterHit = new CharacterHit(offset + characterHit.FirstCharacterIndex, characterHit.TrailingLength);
break;
}
@ -279,7 +278,7 @@ namespace Avalonia.Media.TextFormatting
}
else
{
characterHit = new CharacterHit(currentPosition, run.TextSourceLength);
characterHit = new CharacterHit(currentPosition, run.Length);
}
break;
}
@ -334,14 +333,14 @@ namespace Avalonia.Media.TextFormatting
rightToLeftWidth -= currentRun.Size.Width;
if (currentPosition + currentRun.TextSourceLength >= characterIndex)
if (currentPosition + currentRun.Length >= characterIndex)
{
break;
}
currentPosition += currentRun.TextSourceLength;
currentPosition += currentRun.Length;
remainingLength -= currentRun.TextSourceLength;
remainingLength -= currentRun.Length;
i--;
}
@ -350,7 +349,7 @@ namespace Avalonia.Media.TextFormatting
}
}
if (currentPosition + currentRun.TextSourceLength >= characterIndex &&
if (currentPosition + currentRun.Length >= characterIndex &&
TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _))
{
return Math.Max(0, currentDistance + distance);
@ -358,8 +357,8 @@ namespace Avalonia.Media.TextFormatting
//No hit hit found so we add the full width
currentDistance += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
remainingLength -= currentRun.TextSourceLength;
currentPosition += currentRun.Length;
remainingLength -= currentRun.Length;
}
}
else
@ -383,8 +382,8 @@ namespace Avalonia.Media.TextFormatting
//No hit hit found so we add the full width
currentDistance -= currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
remainingLength -= currentRun.TextSourceLength;
currentPosition += currentRun.Length;
remainingLength -= currentRun.Length;
}
}
@ -412,16 +411,16 @@ namespace Avalonia.Media.TextFormatting
{
currentGlyphRun = shapedTextCharacters.GlyphRun;
if (currentPosition + remainingLength <= currentPosition + currentRun.Text.Length)
if (currentPosition + remainingLength <= currentPosition + currentRun.Length)
{
characterHit = new CharacterHit(currentRun.Text.Start + remainingLength);
characterHit = new CharacterHit(currentPosition + remainingLength);
distance = currentGlyphRun.GetDistanceFromCharacterHit(characterHit);
return true;
}
if (currentPosition + remainingLength == currentPosition + currentRun.Text.Length && isTrailingHit)
if (currentPosition + remainingLength == currentPosition + currentRun.Length && isTrailingHit)
{
if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft)
{
@ -440,7 +439,7 @@ namespace Avalonia.Media.TextFormatting
return true;
}
if (characterIndex == currentPosition + currentRun.TextSourceLength)
if (characterIndex == currentPosition + currentRun.Length)
{
distance = currentRun.Size.Width;
@ -479,17 +478,22 @@ namespace Avalonia.Media.TextFormatting
{
case ShapedTextCharacters shapedRun:
{
characterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
break;
}
default:
{
characterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength);
nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
break;
}
}
return characterHit;
if (characterHit.FirstCharacterIndex + characterHit.TrailingLength == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength)
{
return characterHit;
}
return nextCharacterHit;
}
/// <inheritdoc/>
@ -542,200 +546,182 @@ namespace Avalonia.Media.TextFormatting
var characterLength = 0;
var endX = startX;
var currentShapedRun = currentRun as ShapedTextCharacters;
TextRunBounds currentRunBounds;
double combinedWidth;
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
{
startX += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
continue;
}
if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight)
if (currentRun is ShapedTextCharacters currentShapedRun)
{
var rightToLeftIndex = index;
var rightToLeftWidth = currentShapedRun.Size.Width;
var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun)
if (currentPosition + currentRun.Length <= firstTextSourceIndex)
{
if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
{
break;
}
startX += currentRun.Size.Width;
rightToLeftIndex++;
currentPosition += currentRun.Length;
rightToLeftWidth += nextShapedRun.Size.Width;
if (currentPosition + nextShapedRun.TextSourceLength > firstTextSourceIndex + textLength)
{
break;
}
currentShapedRun = nextShapedRun;
continue;
}
startX = startX + rightToLeftWidth;
if (currentShapedRun.ShapedBuffer.IsLeftToRight)
{
var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition);
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
double startOffset;
remainingLength -= currentRunBounds.Length;
currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
endX = currentRunBounds.Rectangle.Right;
startX = currentRunBounds.Rectangle.Left;
double endOffset;
var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds };
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
for (int i = rightToLeftIndex - 1; i >= index; i--)
{
currentShapedRun = TextRuns[i] as ShapedTextCharacters;
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
if(currentShapedRun == null)
{
continue;
}
startX += startOffset;
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
endX += endOffset;
rightToLeftRunBounds.Insert(0, currentRunBounds);
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
remainingLength -= currentRunBounds.Length;
startX = currentRunBounds.Rectangle.Left;
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
currentPosition += currentRunBounds.Length;
characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
currentDirection = FlowDirection.LeftToRight;
}
else
{
var rightToLeftIndex = index;
var rightToLeftWidth = currentShapedRun.Size.Width;
combinedWidth = endX - startX;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun)
{
if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
{
break;
}
currentRect = new Rect(startX, 0, combinedWidth, Height);
rightToLeftIndex++;
currentDirection = FlowDirection.RightToLeft;
rightToLeftWidth += nextShapedRun.Size.Width;
if (!MathUtilities.IsZero(combinedWidth))
{
result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
}
if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength)
{
break;
}
startX = endX;
}
else
{
if (currentShapedRun != null)
{
var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
currentShapedRun = nextShapedRun;
}
currentPosition += offset;
startX += rightToLeftWidth;
var startIndex = currentRun.Text.Start + offset;
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
double startOffset;
double endOffset;
remainingLength -= currentRunBounds.Length;
currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
endX = currentRunBounds.Rectangle.Right;
startX = currentRunBounds.Rectangle.Left;
if (currentShapedRun.ShapedBuffer.IsLeftToRight)
{
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds };
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
}
else
for (int i = rightToLeftIndex - 1; i >= index; i--)
{
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
if (currentPosition < startIndex)
{
startOffset = endOffset;
}
else
if (TextRuns[i] is not ShapedTextCharacters)
{
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
continue;
}
}
startX += startOffset;
currentShapedRun = (ShapedTextCharacters)TextRuns[i];
endX += endOffset;
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
rightToLeftRunBounds.Insert(0, currentRunBounds);
characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
remainingLength -= currentRunBounds.Length;
startX = currentRunBounds.Rectangle.Left;
currentDirection = FlowDirection.LeftToRight;
}
else
{
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
{
startX += currentRun.Size.Width;
currentPosition += currentRunBounds.Length;
}
currentPosition += currentRun.TextSourceLength;
combinedWidth = endX - startX;
continue;
}
currentRect = new Rect(startX, 0, combinedWidth, Height);
currentDirection = FlowDirection.RightToLeft;
if (currentPosition < firstTextSourceIndex)
if (!MathUtilities.IsZero(combinedWidth))
{
startX += currentRun.Size.Width;
result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
}
if (currentPosition + currentRun.TextSourceLength <= characterIndex)
{
endX += currentRun.Size.Width;
startX = endX;
}
}
else
{
if (currentPosition + currentRun.Length <= firstTextSourceIndex)
{
startX += currentRun.Size.Width;
characterLength = currentRun.TextSourceLength;
}
currentPosition += currentRun.Length;
continue;
}
if (endX < startX)
if (currentPosition < firstTextSourceIndex)
{
(endX, startX) = (startX, endX);
startX += currentRun.Size.Width;
}
//Lines that only contain a linebreak need to be covered here
if (characterLength == 0)
if (currentPosition + currentRun.Length <= characterIndex)
{
characterLength = NewLineLength;
endX += currentRun.Size.Width;
characterLength = currentRun.Length;
}
}
combinedWidth = endX - startX;
if (endX < startX)
{
(endX, startX) = (startX, endX);
}
currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
//Lines that only contain a linebreak need to be covered here
if (characterLength == 0)
{
characterLength = NewLineLength;
}
currentPosition += characterLength;
combinedWidth = endX - startX;
remainingLength -= characterLength;
currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
startX = endX;
currentPosition += characterLength;
if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
{
if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
{
currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
remainingLength -= characterLength;
var textBounds = result[result.Count - 1];
startX = endX;
textBounds.Rectangle = currentRect;
if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
{
if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
{
currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
textBounds.TextRunBounds.Add(currentRunBounds);
}
else
{
currentRect = currentRunBounds.Rectangle;
var textBounds = result[result.Count - 1];
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
}
textBounds.Rectangle = currentRect;
textBounds.TextRunBounds.Add(currentRunBounds);
}
else
{
currentRect = currentRunBounds.Rectangle;
lastRunBounds = currentRunBounds;
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
}
}
lastRunBounds = currentRunBounds;
currentWidth += combinedWidth;
if (remainingLength <= 0 || currentPosition >= characterIndex)
@ -771,11 +757,11 @@ namespace Avalonia.Media.TextFormatting
continue;
}
if (currentPosition + currentRun.TextSourceLength < firstTextSourceIndex)
if (currentPosition + currentRun.Length < firstTextSourceIndex)
{
startX -= currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
currentPosition += currentRun.Length;
continue;
}
@ -789,7 +775,7 @@ namespace Avalonia.Media.TextFormatting
currentPosition += offset;
var startIndex = currentRun.Text.Start + offset;
var startIndex = currentPosition;
double startOffset;
double endOffset;
@ -827,7 +813,7 @@ namespace Avalonia.Media.TextFormatting
}
else
{
if (currentPosition + currentRun.TextSourceLength <= characterIndex)
if (currentPosition + currentRun.Length <= characterIndex)
{
endX -= currentRun.Size.Width;
}
@ -836,7 +822,7 @@ namespace Avalonia.Media.TextFormatting
{
startX -= currentRun.Size.Width;
characterLength = currentRun.TextSourceLength;
characterLength = currentRun.Length;
}
}
@ -905,7 +891,7 @@ namespace Avalonia.Media.TextFormatting
currentPosition += offset;
var startIndex = currentRun.Text.Start + offset;
var startIndex = currentPosition;
double startOffset;
double endOffset;
@ -1172,12 +1158,12 @@ namespace Avalonia.Media.TextFormatting
return true;
}
var characterIndex = codepointIndex - shapedRun.Text.Start;
//var characterIndex = codepointIndex - shapedRun.Text.Start;
if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight)
{
foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
}
//if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight)
//{
// foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
//}
nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
foundCharacterHit :
@ -1196,7 +1182,7 @@ namespace Avalonia.Media.TextFormatting
if (textPosition == currentPosition)
{
nextCharacterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength);
nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
return true;
}
@ -1205,7 +1191,7 @@ namespace Avalonia.Media.TextFormatting
}
}
currentPosition += currentRun.TextSourceLength;
currentPosition += currentRun.Length;
runIndex++;
}
@ -1271,7 +1257,7 @@ namespace Avalonia.Media.TextFormatting
}
default:
{
if (characterIndex == currentPosition + currentRun.TextSourceLength)
if (characterIndex == currentPosition + currentRun.Length)
{
previousCharacterHit = new CharacterHit(currentPosition);
@ -1282,7 +1268,7 @@ namespace Avalonia.Media.TextFormatting
}
}
currentPosition -= currentRun.TextSourceLength;
currentPosition -= currentRun.Length;
runIndex--;
}
@ -1310,18 +1296,25 @@ namespace Avalonia.Media.TextFormatting
{
case ShapedTextCharacters shapedRun:
{
var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
if (firstCluster > codepointIndex)
{
break;
}
if (previousRun is ShapedTextCharacters previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
{
if (shapedRun.ShapedBuffer.IsLeftToRight)
{
if (currentRun.Text.Start >= codepointIndex)
if (firstCluster >= codepointIndex)
{
return --runIndex;
}
}
else
{
if (codepointIndex > currentRun.Text.Start + currentRun.Text.Length)
if (codepointIndex > firstCluster + currentRun.Length)
{
return --runIndex;
}
@ -1330,15 +1323,15 @@ namespace Avalonia.Media.TextFormatting
if (direction == LogicalDirection.Forward)
{
if (codepointIndex >= currentRun.Text.Start && codepointIndex <= currentRun.Text.End)
if (codepointIndex >= firstCluster && codepointIndex <= firstCluster + currentRun.Length)
{
return runIndex;
}
}
else
{
if (codepointIndex > currentRun.Text.Start &&
codepointIndex <= currentRun.Text.Start + currentRun.Text.Length)
if (codepointIndex > firstCluster &&
codepointIndex <= firstCluster + currentRun.Length)
{
return runIndex;
}
@ -1349,6 +1342,8 @@ namespace Avalonia.Media.TextFormatting
return runIndex;
}
textPosition += currentRun.Length;
break;
}
@ -1364,13 +1359,14 @@ namespace Avalonia.Media.TextFormatting
return runIndex;
}
textPosition += currentRun.Length;
break;
}
}
runIndex++;
previousRun = currentRun;
textPosition += currentRun.TextSourceLength;
}
return runIndex;
@ -1401,7 +1397,7 @@ namespace Avalonia.Media.TextFormatting
case ShapedTextCharacters textRun:
{
var textMetrics =
new TextMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize);
if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize)
{
@ -1432,7 +1428,7 @@ namespace Avalonia.Media.TextFormatting
{
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
newLineLength = textRun.GlyphRun.Metrics.NewLineLength;
}
widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;

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

@ -6,13 +6,13 @@
/// </summary>
public readonly struct TextLineMetrics
{
public TextLineMetrics(bool hasOverflowed, double height, int newLineLength, double start, double textBaseline,
public TextLineMetrics(bool hasOverflowed, double height, int newlineLength, double start, double textBaseline,
int trailingWhitespaceLength, double width,
double widthIncludingTrailingWhitespace)
{
HasOverflowed = hasOverflowed;
Height = height;
NewLineLength = newLineLength;
NewlineLength = newlineLength;
Start = start;
TextBaseline = textBaseline;
TrailingWhitespaceLength = trailingWhitespaceLength;
@ -33,7 +33,7 @@
/// <summary>
/// Gets the number of newline characters at the end of a line.
/// </summary>
public int NewLineLength { get; }
public int NewlineLength { get; }
/// <summary>
/// Gets the distance from the start of a paragraph to the starting point of a line.

4
src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs

@ -5,9 +5,9 @@
/// </summary>
public readonly struct TextMetrics
{
public TextMetrics(Typeface typeface, double fontRenderingEmSize)
public TextMetrics(IGlyphTypeface glyphTypeface, double fontRenderingEmSize)
{
var fontMetrics = typeface.GlyphTypeface.Metrics;
var fontMetrics = glyphTypeface.Metrics;
var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight;

11
src/Avalonia.Base/Media/TextFormatting/TextRun.cs

@ -1,5 +1,4 @@
using System.Diagnostics;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -14,12 +13,12 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Gets the text source length.
/// </summary>
public virtual int TextSourceLength => DefaultTextSourceLength;
public virtual int Length => DefaultTextSourceLength;
/// <summary>
/// Gets the text run's text.
/// </summary>
public virtual ReadOnlySlice<char> Text => default;
public virtual CharacterBufferReference CharacterBufferReference => default;
/// <summary>
/// A set of properties shared by every characters in the run
@ -41,9 +40,11 @@ namespace Avalonia.Media.TextFormatting
{
unsafe
{
fixed (char* charsPtr = _textRun.Text.Span)
var characterBuffer = _textRun.CharacterBufferReference.CharacterBuffer;
fixed (char* charsPtr = characterBuffer.Span)
{
return new string(charsPtr, 0, _textRun.Text.Length);
return new string(charsPtr, 0, characterBuffer.Span.Length);
}
}
}

11
src/Avalonia.Base/Media/TextFormatting/TextShaper.cs

@ -1,7 +1,5 @@
using System;
using System.Globalization;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -45,9 +43,14 @@ namespace Avalonia.Media.TextFormatting
}
/// <inheritdoc cref="ITextShaperImpl.ShapeText"/>
public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options = default)
{
return _platformImpl.ShapeText(text, options);
return _platformImpl.ShapeText(text, length, options);
}
public ShapedBuffer ShapeText(string text, TextShaperOptions options = default)
{
return ShapeText(new CharacterBufferReference(text), text.Length, options);
}
}
}

3
src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

@ -1,5 +1,4 @@
using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -15,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="ellipsis">Text used as collapsing symbol.</param>
/// <param name="width">Width in which collapsing is constrained to.</param>
/// <param name="textRunProperties">Text run properties of ellipsis symbol.</param>
public TextTrailingCharacterEllipsis(ReadOnlySlice<char> ellipsis, double width, TextRunProperties textRunProperties)
public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties)
{
Width = width;
Symbol = new TextCharacters(ellipsis, textRunProperties);

2
src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs

@ -16,7 +16,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="width">width in which collapsing is constrained to.</param>
/// <param name="textRunProperties">text run properties of ellipsis symbol.</param>
public TextTrailingWordEllipsis(
ReadOnlySlice<char> ellipsis,
string ellipsis,
double width,
TextRunProperties textRunProperties
)

3
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
// Ported from: https://github.com/SixLabors/Fonts/
using System;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
@ -63,7 +64,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// Appends text to the bidi data.
/// </summary>
/// <param name="text">The text to process.</param>
public void Append(ReadOnlySlice<char> text)
public void Append(CharacterBufferRange text)
{
_classes.Add(text.Length);
_pairedBracketTypes.Add(text.Length);

9
src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
{
@ -166,11 +165,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <param name="index">The index to read at.</param>
/// <param name="count">The count of character that were read.</param>
/// <returns></returns>
public static Codepoint ReadAt(ReadOnlySpan<char> text, int index, out int count)
public static Codepoint ReadAt(IReadOnlyList<char> text, int index, out int count)
{
count = 1;
if (index >= text.Length)
if (index >= text.Count)
{
return ReplacementCodepoint;
}
@ -184,7 +183,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
{
hi = code;
if (index + 1 == text.Length)
if (index + 1 == text.Count)
{
return ReplacementCodepoint;
}

7
src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs

@ -1,12 +1,13 @@
using Avalonia.Utilities;
using System;
namespace Avalonia.Media.TextFormatting.Unicode
{
public ref struct CodepointEnumerator
{
private ReadOnlySlice<char> _text;
private CharacterBufferRange _text;
private int _pos;
public CodepointEnumerator(ReadOnlySlice<char> text)
public CodepointEnumerator(CharacterBufferRange text)
{
_text = text;
Current = Codepoint.ReplacementCodepoint;

8
src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs

@ -1,13 +1,13 @@
using Avalonia.Utilities;
using System;
namespace Avalonia.Media.TextFormatting.Unicode
{
/// <summary>
/// Represents the smallest unit of a writing system of any given language.
/// </summary>
public readonly struct Grapheme
public readonly ref struct Grapheme
{
public Grapheme(Codepoint firstCodepoint, ReadOnlySlice<char> text)
public Grapheme(Codepoint firstCodepoint, ReadOnlySpan<char> text)
{
FirstCodepoint = firstCodepoint;
Text = text;
@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary>
/// The text that is representing the <see cref="Grapheme"/>.
/// </summary>
public ReadOnlySlice<char> Text { get; }
public ReadOnlySpan<char> Text { get; }
}
}

12
src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs

@ -3,16 +3,16 @@
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
{
public ref struct GraphemeEnumerator
{
private ReadOnlySlice<char> _text;
private CharacterBufferRange _text;
public GraphemeEnumerator(ReadOnlySlice<char> text)
public GraphemeEnumerator(CharacterBufferRange text)
{
_text = text;
Current = default;
@ -187,7 +187,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
var text = _text.Take(processor.CurrentCodeUnitOffset);
Current = new Grapheme(firstCodepoint, text);
Current = new Grapheme(firstCodepoint, text.Span);
_text = _text.Skip(processor.CurrentCodeUnitOffset);
@ -197,10 +197,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
[StructLayout(LayoutKind.Auto)]
private ref struct Processor
{
private readonly ReadOnlySlice<char> _buffer;
private readonly CharacterBufferRange _buffer;
private int _codeUnitLengthOfCurrentScalar;
internal Processor(ReadOnlySlice<char> buffer)
internal Processor(CharacterBufferRange buffer)
{
_buffer = buffer;
_codeUnitLengthOfCurrentScalar = 0;

15
src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs

@ -2,7 +2,8 @@
// Licensed under the Apache License, Version 2.0.
// Ported from: https://github.com/SixLabors/Fonts/
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting.Unicode
{
@ -12,7 +13,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </summary>
public ref struct LineBreakEnumerator
{
private readonly ReadOnlySlice<char> _text;
private readonly IReadOnlyList<char> _text;
private int _position;
private int _lastPosition;
private LineBreakClass _currentClass;
@ -28,7 +29,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
private int _lb30a;
private bool _lb31;
public LineBreakEnumerator(ReadOnlySlice<char> text)
public LineBreakEnumerator(IReadOnlyList<char> text)
: this()
{
_text = text;
@ -62,7 +63,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
_lb30a = 0;
}
while (_position < _text.Length)
while (_position < _text.Count)
{
_lastPosition = _position;
var lastClass = _nextClass;
@ -92,11 +93,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
}
}
if (_position >= _text.Length)
if (_position >= _text.Count)
{
if (_lastPosition < _text.Length)
if (_lastPosition < _text.Count)
{
_lastPosition = _text.Length;
_lastPosition = _text.Count;
var required = false;

11
src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs

@ -1,21 +1,16 @@
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Media
{
public sealed class TextLeadingPrefixTrimming : TextTrimming
{
private readonly ReadOnlySlice<char> _ellipsis;
private readonly string _ellipsis;
private readonly int _prefixLength;
public TextLeadingPrefixTrimming(char ellipsis, int prefixLength) : this(new[] { ellipsis }, prefixLength)
{
}
public TextLeadingPrefixTrimming(char[] ellipsis, int prefixLength)
public TextLeadingPrefixTrimming(string ellipsis, int prefixLength)
{
_prefixLength = prefixLength;
_ellipsis = new ReadOnlySlice<char>(ellipsis);
_ellipsis = ellipsis;
}
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)

11
src/Avalonia.Base/Media/TextTrailingTrimming.cs

@ -1,21 +1,16 @@
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Media
{
public sealed class TextTrailingTrimming : TextTrimming
{
private readonly ReadOnlySlice<char> _ellipsis;
private readonly string _ellipsis;
private readonly bool _isWordBased;
public TextTrailingTrimming(char ellipsis, bool isWordBased) : this(new[] {ellipsis}, isWordBased)
{
}
public TextTrailingTrimming(char[] ellipsis, bool isWordBased)
public TextTrailingTrimming(string ellipsis, bool isWordBased)
{
_isWordBased = isWordBased;
_ellipsis = new ReadOnlySlice<char>(ellipsis);
_ellipsis = ellipsis;
}
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)

2
src/Avalonia.Base/Media/TextTrimming.cs

@ -8,7 +8,7 @@ namespace Avalonia.Media
/// </summary>
public abstract class TextTrimming
{
internal const char DefaultEllipsisChar = '\u2026';
internal const string DefaultEllipsisChar = "\u2026";
/// <summary>
/// Text is not trimmed.

5
src/Avalonia.Base/Platform/ITextShaperImpl.cs

@ -1,6 +1,5 @@
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Platform
{
@ -13,9 +12,9 @@ namespace Avalonia.Platform
/// <summary>
/// Shapes the specified region within the text and returns a shaped buffer.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="text">The text buffer.</param>
/// <param name="options">Text shaper options to customize the shaping process.</param>
/// <returns>A shaped glyph run.</returns>
ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options);
ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options);
}
}

3
src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs

@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
@ -31,7 +32,7 @@ internal class FpsCounter
{
var s = new string((char)c, 1);
var glyph = typeface.GetGlyph((uint)(s[0]));
_runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice<char>(s.AsMemory()), new ushort[] { glyph });
_runs[c - FirstChar] = new GlyphRun(typeface, 18, s.ToArray(), new ushort[] { glyph });
}
}

8
src/Avalonia.Base/Utilities/ArraySlice.cs

@ -111,14 +111,6 @@ namespace Avalonia.Utilities
}
}
/// <summary>
/// Defines an implicit conversion of a <see cref="ArraySlice{T}"/> to a <see cref="ReadOnlySlice{T}"/>
/// </summary>
public static implicit operator ReadOnlySlice<T>(ArraySlice<T> slice)
{
return new ReadOnlySlice<T>(slice._data, 0, slice.Length, slice.Start);
}
/// <summary>
/// Defines an implicit conversion of an array to a <see cref="ArraySlice{T}"/>
/// </summary>

239
src/Avalonia.Base/Utilities/ReadOnlySlice.cs

@ -1,239 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Avalonia.Utilities
{
/// <summary>
/// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region.
/// </summary>
/// <typeparam name="T">The type of elements in the slice.</typeparam>
[DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))]
public readonly struct ReadOnlySlice<T> : IReadOnlyList<T> where T : struct
{
private readonly int _bufferOffset;
/// <summary>
/// Gets an empty <see cref="ReadOnlySlice{T}"/>
/// </summary>
public static ReadOnlySlice<T> Empty => new ReadOnlySlice<T>(Array.Empty<T>());
private readonly ReadOnlyMemory<T> _buffer;
public ReadOnlySlice(ReadOnlyMemory<T> buffer) : this(buffer, 0, buffer.Length) { }
public ReadOnlySlice(ReadOnlyMemory<T> buffer, int start, int length, int bufferOffset = 0)
{
#if DEBUG
if (start.CompareTo(0) < 0)
{
throw new ArgumentOutOfRangeException(nameof (start));
}
if (length.CompareTo(buffer.Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof (length));
}
#endif
_buffer = buffer;
Start = start;
Length = length;
_bufferOffset = bufferOffset;
}
/// <summary>
/// Gets the start.
/// </summary>
/// <value>
/// The start.
/// </value>
public int Start { get; }
/// <summary>
/// Gets the end.
/// </summary>
/// <value>
/// The end.
/// </value>
public int End => Start + Length - 1;
/// <summary>
/// Gets the length.
/// </summary>
/// <value>
/// The length.
/// </value>
public int Length { get; }
/// <summary>
/// Gets a value that indicates whether this instance of <see cref="ReadOnlySlice{T}"/> is Empty.
/// </summary>
public bool IsEmpty => Length == 0;
/// <summary>
/// Get the underlying span.
/// </summary>
public ReadOnlySpan<T> Span => _buffer.Span.Slice(_bufferOffset, Length);
/// <summary>
/// Get the buffer offset.
/// </summary>
public int BufferOffset => _bufferOffset;
/// <summary>
/// Get the underlying buffer.
/// </summary>
public ReadOnlyMemory<T> Buffer => _buffer;
/// <summary>
/// Returns a value to specified element of the slice.
/// </summary>
/// <param name="index">The index of the element to return.</param>
/// <returns>The <typeparamref name="T"/>.</returns>
/// <exception cref="IndexOutOfRangeException">
/// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
/// </exception>
public T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
#if DEBUG
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof (index));
}
#endif
return Span[index];
}
}
/// <summary>
/// Returns a sub slice of elements that start at the specified index and has the specified number of elements.
/// </summary>
/// <param name="start">The start of the sub slice.</param>
/// <param name="length">The length of the sub slice.</param>
/// <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 (length == 0)
{
return Empty;
}
if (start < 0 || _bufferOffset + start > _buffer.Length - 1)
{
throw new ArgumentOutOfRangeException(nameof(start));
}
if (_bufferOffset + start + length > _buffer.Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ReadOnlySlice<T>(_buffer, start, length, _bufferOffset);
}
/// <summary>
/// Returns a specified number of contiguous elements from the start of the slice.
/// </summary>
/// <param name="length">The number of elements to return.</param>
/// <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));
}
return new ReadOnlySlice<T>(_buffer, Start, length, _bufferOffset);
}
/// <summary>
/// Bypasses a specified number of elements in the slice and then returns the remaining elements.
/// </summary>
/// <param name="length">The number of elements to skip before returning the remaining elements.</param>
/// <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));
}
return new ReadOnlySlice<T>(_buffer, Start + length, Length - length, _bufferOffset + length);
}
/// <summary>
/// Returns an enumerator for the slice.
/// </summary>
public ImmutableReadOnlyListStructEnumerator<T> GetEnumerator()
{
return new ImmutableReadOnlyListStructEnumerator<T>(this);
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
int IReadOnlyCollection<T>.Count => Length;
T IReadOnlyList<T>.this[int index] => this[index];
public static implicit operator ReadOnlySlice<T>(T[] array)
{
return new ReadOnlySlice<T>(array);
}
public static implicit operator ReadOnlySlice<T>(ReadOnlyMemory<T> memory)
{
return new ReadOnlySlice<T>(memory);
}
public static implicit operator ReadOnlySpan<T>(ReadOnlySlice<T> slice) => slice.Span;
internal class ReadOnlySliceDebugView
{
private readonly ReadOnlySlice<T> _readOnlySlice;
public ReadOnlySliceDebugView(ReadOnlySlice<T> readOnlySlice)
{
_readOnlySlice = readOnlySlice;
}
public int Start => _readOnlySlice.Start;
public int End => _readOnlySlice.End;
public int Length => _readOnlySlice.Length;
public bool IsEmpty => _readOnlySlice.IsEmpty;
public ReadOnlySpan<T> Items => _readOnlySlice.Span;
}
}
}

4
src/Avalonia.Controls/Documents/LineBreak.cs

@ -4,7 +4,7 @@ using System.Text;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
namespace Avalonia.Controls.Documents
namespace Avalonia.Controls.Documents
{
/// <summary>
/// LineBreak element that forces a line breaking.
@ -21,7 +21,7 @@ namespace Avalonia.Controls.Documents
internal override void BuildTextRun(IList<TextRun> textRuns)
{
var text = Environment.NewLine.AsMemory();
var text = Environment.NewLine;
var textRunProperties = CreateTextRunProperties();

2
src/Avalonia.Controls/Documents/Run.cs

@ -52,7 +52,7 @@ namespace Avalonia.Controls.Documents
internal override void BuildTextRun(IList<TextRun> textRuns)
{
var text = (Text ?? "").AsMemory();
var text = Text ?? "";
var textRunProperties = CreateTextRunProperties();

27
src/Avalonia.Controls/TextBlock.cs

@ -630,7 +630,7 @@ namespace Avalonia.Controls
}
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
textSource = new SimpleTextSource(text ?? "", defaultProperties);
}
return new TextLayout(
@ -829,12 +829,12 @@ namespace Avalonia.Controls
protected readonly struct SimpleTextSource : ITextSource
{
private readonly ReadOnlySlice<char> _text;
private readonly CharacterBufferRange _text;
private readonly TextRunProperties _defaultProperties;
public SimpleTextSource(ReadOnlySlice<char> text, TextRunProperties defaultProperties)
public SimpleTextSource(string text, TextRunProperties defaultProperties)
{
_text = text;
_text = new CharacterBufferRange(new CharacterBufferReference(text), text.Length);
_defaultProperties = defaultProperties;
}
@ -852,7 +852,7 @@ namespace Avalonia.Controls
return new TextEndOfParagraph();
}
return new TextCharacters(runText, _defaultProperties);
return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultProperties);
}
}
@ -873,21 +873,28 @@ namespace Avalonia.Controls
foreach (var textRun in _textRuns)
{
if (textRun.TextSourceLength == 0)
if (textRun.Length == 0)
{
continue;
}
if (textSourceIndex >= currentPosition + textRun.TextSourceLength)
if (textSourceIndex >= currentPosition + textRun.Length)
{
currentPosition += textRun.TextSourceLength;
currentPosition += textRun.Length;
continue;
}
if (textRun is TextCharacters)
if (textRun is TextCharacters)
{
return new TextCharacters(textRun.Text.Skip(Math.Max(0, textSourceIndex - currentPosition)), textRun.Properties!);
var characterBufferReference = textRun.CharacterBufferReference;
var skip = Math.Max(0, textSourceIndex - currentPosition);
return new TextCharacters(
new CharacterBufferReference(characterBufferReference.CharacterBuffer, characterBufferReference.OffsetToFirstChar + skip),
textRun.Length - skip,
textRun.Properties!);
}
return textRun;

4
src/Avalonia.Controls/TextBox.cs

@ -961,7 +961,9 @@ namespace Avalonia.Controls
var length = 0;
var graphemeEnumerator = new GraphemeEnumerator(input.AsMemory());
var inputRange = new CharacterBufferRange(new CharacterBufferReference(input), input.Length);
var graphemeEnumerator = new GraphemeEnumerator(inputRange);
while (graphemeEnumerator.MoveNext())
{

8
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -77,12 +77,14 @@ namespace Avalonia.Controls
foreach (var run in textLine.TextRuns)
{
if(run.Text.Length > 0)
if(run.Length > 0)
{
var characterBufferRange = new CharacterBufferRange(run.CharacterBufferReference, run.Length);
#if NET6_0
builder.Append(run.Text.Span);
builder.Append(characterBufferRange.Span);
#else
builder.Append(run.Text.Span.ToArray());
builder.Append(characterBufferRange.Span.ToArray());
#endif
}
}

6
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -145,13 +145,15 @@ namespace Avalonia.Headless
class HeadlessTextShaperStub : ITextShaperImpl
{
public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options)
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
var characterBufferRange = new CharacterBufferRange(text, length);
return new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel);
}
}

24
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -3,7 +3,6 @@ using System.Globalization;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
using HarfBuzzSharp;
using Buffer = HarfBuzzSharp.Buffer;
using GlyphInfo = HarfBuzzSharp.GlyphInfo;
@ -12,8 +11,9 @@ namespace Avalonia.Skia
{
internal class TextShaperImpl : ITextShaperImpl
{
public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options)
{
var text = new CharacterBufferRange(characterBufferReference, length);
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
@ -21,21 +21,21 @@ namespace Avalonia.Skia
using (var buffer = new Buffer())
{
buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length);
buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, length);
MergeBreakPair(buffer);
buffer.GuessSegmentProperties();
buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft;
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
var font = ((GlyphTypefaceImpl)typeface).Font;
font.Shape(buffer);
if(buffer.Direction == Direction.RightToLeft)
if (buffer.Direction == Direction.RightToLeft)
{
buffer.Reverse();
}
@ -64,12 +64,12 @@ namespace Avalonia.Skia
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
if(text.Buffer.Span[glyphCluster] == '\t')
if (text[i] == '\t')
{
glyphIndex = typeface.GetGlyph(' ');
glyphAdvance = options.IncrementalTabWidth > 0 ?
options.IncrementalTabWidth :
glyphAdvance = options.IncrementalTabWidth > 0 ?
options.IncrementalTabWidth :
4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
}
@ -87,7 +87,7 @@ namespace Avalonia.Skia
var length = buffer.Length;
var glyphInfos = buffer.GetGlyphInfoSpan();
var second = glyphInfos[length - 1];
if (!new Codepoint(second.Codepoint).IsBreakChar)
@ -98,7 +98,7 @@ namespace Avalonia.Skia
if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n')
{
var first = glyphInfos[length - 2];
first.Codepoint = '\u200C';
second.Codepoint = '\u200C';
second.Cluster = first.Cluster;
@ -109,7 +109,7 @@ namespace Avalonia.Skia
{
*p = first;
}
fixed (GlyphInfo* p = &glyphInfos[length - 1])
{
*p = second;

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

@ -3,7 +3,6 @@ using System.Globalization;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
using HarfBuzzSharp;
using Buffer = HarfBuzzSharp.Buffer;
using GlyphInfo = HarfBuzzSharp.GlyphInfo;
@ -12,7 +11,7 @@ namespace Avalonia.Direct2D1.Media
{
internal class TextShaperImpl : ITextShaperImpl
{
public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options)
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
@ -21,7 +20,7 @@ namespace Avalonia.Direct2D1.Media
using (var buffer = new Buffer())
{
buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length);
buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, length);
MergeBreakPair(buffer);
@ -46,7 +45,9 @@ namespace Avalonia.Direct2D1.Media
var bufferLength = buffer.Length;
var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
var characterBufferRange = new CharacterBufferRange(characterBufferReference, length);
var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
var glyphInfos = buffer.GetGlyphInfoSpan();
@ -64,7 +65,7 @@ namespace Avalonia.Direct2D1.Media
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
if (text.Buffer.Span[glyphCluster] == '\t')
if (characterBufferRange[i] == '\t')
{
glyphIndex = typeface.GetGlyph(' ');

4
tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs

@ -181,9 +181,7 @@ namespace Avalonia.Base.UnitTests.Media
var count = glyphAdvances.Length;
var glyphIndices = new ushort[count];
var start = bidiLevel == 0 ? glyphClusters[0] : glyphClusters[^1];
var characters = new ReadOnlySlice<char>(Enumerable.Repeat('a', count).ToArray(), start, count);
var characters = Enumerable.Repeat('a', count).ToArray();
return new GlyphRun(new MockGlyphTypeface(), 10, characters, glyphIndices, glyphAdvances,
glyphClusters: glyphClusters, biDiLevel: bidiLevel);

3
tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs

@ -2,6 +2,7 @@
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Xunit;
using Xunit.Abstractions;
@ -36,7 +37,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
var text = Encoding.UTF32.GetString(MemoryMarshal.Cast<int, byte>(t.CodePoints).ToArray());
// Append
bidiData.Append(text.AsMemory());
bidiData.Append(new CharacterBufferRange(text));
// Act
for (int i = 0; i < 10; i++)

9
tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs

@ -1,6 +1,7 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Visuals.UnitTests.Media.TextFormatting;
using Xunit;
@ -37,11 +38,11 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
var text = Encoding.UTF32.GetString(MemoryMarshal.Cast<int, byte>(t.Codepoints).ToArray());
var grapheme = Encoding.UTF32.GetString(MemoryMarshal.Cast<int, byte>(t.Grapheme).ToArray()).AsSpan();
var enumerator = new GraphemeEnumerator(text.AsMemory());
var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text));
enumerator.MoveNext();
var actual = enumerator.Current.Text.Span;
var actual = enumerator.Current.Text;
var pass = true;
@ -86,9 +87,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
{
const string text = "ABCDEFGHIJ";
var textMemory = text.AsMemory();
var enumerator = new GraphemeEnumerator(textMemory);
var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text));
var count = 0;

9
tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Xunit;
using Xunit.Abstractions;
@ -22,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
[Fact]
public void BasicLatinTest()
{
var lineBreaker = new LineBreakEnumerator("Hello World\r\nThis is a test.".AsMemory());
var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange("Hello World\r\nThis is a test."));
Assert.True(lineBreaker.MoveNext());
Assert.Equal(6, lineBreaker.Current.PositionWrap);
@ -55,7 +56,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
[Fact]
public void ForwardTextWithOuterWhitespace()
{
var lineBreaker = new LineBreakEnumerator(" Apples Pears Bananas ".AsMemory());
var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(" Apples Pears Bananas "));
var positionsF = GetBreaks(lineBreaker);
Assert.Equal(1, positionsF[0].PositionWrap);
Assert.Equal(0, positionsF[0].PositionMeasure);
@ -82,7 +83,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
[Fact]
public void ForwardTest()
{
var lineBreaker = new LineBreakEnumerator("Apples Pears Bananas".AsMemory());
var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange("Apples Pears Bananas"));
var positionsF = GetBreaks(lineBreaker);
Assert.Equal(7, positionsF[0].PositionWrap);
@ -99,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
{
var text = string.Join(null, codePoints.Select(char.ConvertFromUtf32));
var lineBreaker = new LineBreakEnumerator(text.AsMemory());
var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(text));
var foundBreaks = new List<int>();

37
tests/Avalonia.Base.UnitTests/Utilities/ReadOnlySpanTests.cs

@ -1,37 +0,0 @@
using System.Linq;
using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Base.UnitTests.Utilities
{
public class ReadOnlySpanTests
{
[Fact]
public void Should_Skip()
{
var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var slice = new ReadOnlySlice<int>(buffer);
var skipped = slice.Skip(2);
var expected = buffer.Skip(2);
Assert.Equal(expected, skipped);
}
[Fact]
public void Should_Take()
{
var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var slice = new ReadOnlySlice<int>(buffer);
var taken = slice.Take(8);
var expected = buffer.Take(8);
Assert.Equal(expected, taken);
}
}
}

2
tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs

@ -46,7 +46,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.NotNull(target.TextLayout);
var actual = string.Join(null,
target.TextLayout.TextLines.SelectMany(x => x.TextRuns).Select(x => x.Text.Span.ToString()));
target.TextLayout.TextLines.SelectMany(x => x.TextRuns).Select(x => x.CharacterBufferReference.CharacterBuffer.Span.ToString()));
Assert.Equal("****", actual);
}

29
tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs

@ -19,7 +19,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture);
var shapedBuffer =
TextShaper.Current.ShapeText(text.AsMemory(), options);
TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options);
var glyphRun = CreateGlyphRun(shapedBuffer);
@ -39,8 +39,6 @@ namespace Avalonia.Skia.UnitTests.Media
}
else
{
shapedBuffer.GlyphInfos.Span.Reverse();
foreach (var rect in rects)
{
characterHit = glyphRun.GetNextCaretCharacterHit(characterHit);
@ -62,7 +60,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture);
var shapedBuffer =
TextShaper.Current.ShapeText(text.AsMemory(), options);
TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options);
var glyphRun = CreateGlyphRun(shapedBuffer);
@ -84,8 +82,6 @@ namespace Avalonia.Skia.UnitTests.Media
}
else
{
shapedBuffer.GlyphInfos.Span.Reverse();
foreach (var rect in rects)
{
characterHit = glyphRun.GetPreviousCaretCharacterHit(characterHit);
@ -107,7 +103,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture);
var shapedBuffer =
TextShaper.Current.ShapeText(text.AsMemory(), options);
TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options);
var glyphRun = CreateGlyphRun(shapedBuffer);
@ -116,16 +112,14 @@ namespace Avalonia.Skia.UnitTests.Media
var characterHit =
glyphRun.GetCharacterHitFromDistance(glyphRun.Metrics.WidthIncludingTrailingWhitespace, out _);
Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
else
{
shapedBuffer.GlyphInfos.Span.Reverse();
var characterHit =
var characterHit =
glyphRun.GetCharacterHitFromDistance(0, out _);
Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
var rects = BuildRects(glyphRun);
@ -218,15 +212,22 @@ namespace Avalonia.Skia.UnitTests.Media
private static GlyphRun CreateGlyphRun(ShapedBuffer shapedBuffer)
{
return new GlyphRun(
var glyphRun = new GlyphRun(
shapedBuffer.GlyphTypeface,
shapedBuffer.FontRenderingEmSize,
shapedBuffer.Text,
shapedBuffer.CharacterBufferRange,
shapedBuffer.GlyphIndices,
shapedBuffer.GlyphAdvances,
shapedBuffer.GlyphOffsets,
shapedBuffer.GlyphClusters,
shapedBuffer.BidiLevel);
if(shapedBuffer.BidiLevel == 1)
{
shapedBuffer.GlyphInfos.Span.Reverse();
}
return glyphRun;
}
private static IDisposable Start()

3
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs

@ -29,8 +29,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var runText = _runTexts[index];
return new TextCharacters(
new ReadOnlySlice<char>(runText.AsMemory(), textSourceIndex, runText.Length), _defaultStyle);
return new TextCharacters(runText, _defaultStyle);
}
}
}

19
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs

@ -1,30 +1,33 @@
using System;
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
using Avalonia.Media.TextFormatting;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
internal class SingleBufferTextSource : ITextSource
{
private readonly ReadOnlySlice<char> _text;
private readonly CharacterBufferRange _text;
private readonly GenericTextRunProperties _defaultGenericPropertiesRunProperties;
public SingleBufferTextSource(string text, GenericTextRunProperties defaultProperties)
{
_text = text.AsMemory();
_text = new CharacterBufferRange(text);
_defaultGenericPropertiesRunProperties = defaultProperties;
}
public TextRun GetTextRun(int textSourceIndex)
{
if (textSourceIndex > _text.Length)
if (textSourceIndex >= _text.Length)
{
return null;
}
var runText = _text.Skip(textSourceIndex);
return runText.IsEmpty ? null : new TextCharacters(runText, _defaultGenericPropertiesRunProperties);
if (runText.IsEmpty)
{
return null;
}
return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultGenericPropertiesRunProperties);
}
}
}

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

@ -37,7 +37,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(defaultProperties.ForegroundBrush, textRun.Properties.ForegroundBrush);
Assert.Equal(text.Length, textRun.Text.Length);
Assert.Equal(text.Length, textRun.Length);
}
}
@ -82,7 +82,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new ValueSpan<TextRunProperties>(9, 1, defaultProperties)
};
var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, GenericTextRunPropertiesRuns);
var textSource = new FormattedTextSource(text, defaultProperties, GenericTextRunPropertiesRuns);
var formatter = new TextFormatterImpl();
@ -97,7 +97,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textRun = textLine.TextRuns[i];
Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Text.Length);
Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Length);
}
}
}
@ -166,7 +166,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var firstRun = textLine.TextRuns[0];
Assert.Equal(4, firstRun.Text.Length);
Assert.Equal(4, firstRun.Length);
}
}
@ -216,7 +216,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
using (Start())
{
var lineBreaker = new LineBreakEnumerator(text.AsMemory());
var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(text));
var expected = new List<int>();
@ -369,7 +369,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new GenericTextRunProperties(new Typeface("Verdana", FontStyle.Italic),32))
};
var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, styleSpans);
var textSource = new FormattedTextSource(text, defaultProperties, styleSpans);
var formatter = new TextFormatterImpl();
@ -389,7 +389,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
{
textLine = textLine.Collapse(new TextTrailingWordEllipsis(new ReadOnlySlice<char>(new[] { TextTrimming.DefaultEllipsisChar }), 300, defaultProperties));
textLine = textLine.Collapse(new TextTrailingWordEllipsis(TextTrimming.DefaultEllipsisChar, 300, defaultProperties));
}
currentHeight += textLine.Height;
@ -472,7 +472,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, textPosition, 50, paragraphProperties, lastBreak);
Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.TextSourceLength));
Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.Length));
textPosition += textLine.Length;
@ -534,7 +534,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
};
var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, spans);
var textSource = new FormattedTextSource(text, defaultProperties, spans);
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
@ -614,8 +614,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
return new RectangleRun(new Rect(0, 0, 50, 50), Brushes.Green);
}
return new TextCharacters(_text.AsMemory(),
new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black));
return new TextCharacters(_text, 0, _text.Length, new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black));
}
}

46
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@ -60,9 +60,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textRun = textLine.TextRuns[1];
Assert.Equal(2, textRun.Text.Length);
Assert.Equal(2, textRun.Length);
var actual = textRun.Text.Span.ToString();
var actual = new CharacterBufferRange(textRun).Span.ToString();
Assert.Equal("1 ", actual);
@ -144,8 +144,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast<ShapedTextCharacters>()
.SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
var outer = new GraphemeEnumerator(text.AsMemory());
var inner = new GraphemeEnumerator(text.AsMemory());
var outer = new GraphemeEnumerator(new CharacterBufferRange(text));
var inner = new GraphemeEnumerator(new CharacterBufferRange(text));
var i = 0;
var j = 0;
@ -190,7 +190,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
break;
}
inner = new GraphemeEnumerator(text.AsMemory());
inner = new GraphemeEnumerator(new CharacterBufferRange(text));
i += outer.Current.Text.Length;
}
@ -223,10 +223,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textRun = textLine.TextRuns[0];
Assert.Equal(2, textRun.Text.Length);
Assert.Equal(2, textRun.Length);
var actual = SingleLineText.Substring(textRun.Text.Start,
textRun.Text.Length);
var actual = SingleLineText[..textRun.Length];
Assert.Equal("01", actual);
@ -260,9 +259,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textRun = textLine.TextRuns[1];
Assert.Equal(2, textRun.Text.Length);
Assert.Equal(2, textRun.Length);
var actual = textRun.Text.Span.ToString();
var actual = new CharacterBufferRange(textRun).Span.ToString();
Assert.Equal("89", actual);
@ -296,7 +295,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textRun = textLine.TextRuns[0];
Assert.Equal(1, textRun.Text.Length);
Assert.Equal(1, textRun.Length);
Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
}
@ -330,9 +329,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textRun = textLine.TextRuns[1];
Assert.Equal(2, textRun.Text.Length);
Assert.Equal(2, textRun.Length);
var actual = textRun.Text.Span.ToString();
var actual = new CharacterBufferRange(textRun).Span.ToString();
Assert.Equal("😄", actual);
@ -369,7 +368,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(
MultiLineText.Length,
layout.TextLines.Select(textLine =>
textLine.TextRuns.Sum(textRun => textRun.Text.Length))
textLine.TextRuns.Sum(textRun => textRun.Length))
.Sum());
}
}
@ -402,7 +401,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(
text.Length,
layout.TextLines.Select(textLine =>
textLine.TextRuns.Sum(textRun => textRun.Text.Length))
textLine.TextRuns.Sum(textRun => textRun.Length))
.Sum());
}
}
@ -558,7 +557,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textRun = (ShapedTextCharacters)textLine.TextRuns[0];
Assert.Equal(7, textRun.Text.Length);
Assert.Equal(7, textRun.Length);
var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint);
@ -668,10 +667,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(5, layout.TextLines.Count);
Assert.Equal("123\r\n", layout.TextLines[0].TextRuns[0].Text);
Assert.Equal("\r\n", layout.TextLines[1].TextRuns[0].Text);
Assert.Equal("456\r\n", layout.TextLines[2].TextRuns[0].Text);
Assert.Equal("\r\n", layout.TextLines[3].TextRuns[0].Text);
Assert.Equal("123\r\n", new CharacterBufferRange(layout.TextLines[0].TextRuns[0]));
Assert.Equal("\r\n", new CharacterBufferRange(layout.TextLines[1].TextRuns[0]));
Assert.Equal("456\r\n", new CharacterBufferRange(layout.TextLines[2].TextRuns[0]));
Assert.Equal("\r\n", new CharacterBufferRange(layout.TextLines[3].TextRuns[0]));
}
}
@ -815,7 +814,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
Assert.True(textLine.Width <= maxWidth);
var actual = new string(textLine.TextRuns.Cast<ShapedTextCharacters>().OrderBy(x => x.Text.Start).SelectMany(x => x.Text).ToArray());
var actual = new string(textLine.TextRuns.Cast<ShapedTextCharacters>()
.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar)
.SelectMany(x => new CharacterBufferRange(x.CharacterBufferReference, x.Length)).ToArray());
var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length);
Assert.Equal(expected, actual);
@ -966,7 +968,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var i = 0;
var graphemeEnumerator = new GraphemeEnumerator(text.AsMemory());
var graphemeEnumerator = new GraphemeEnumerator(new CharacterBufferRange(text));
while (graphemeEnumerator.MoveNext())
{

90
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -90,7 +90,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var clusters = new List<int>();
foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start))
foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar))
{
var shapedRun = (ShapedTextCharacters)textRun;
@ -137,7 +137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var clusters = new List<int>();
foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start))
foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar))
{
var shapedRun = (ShapedTextCharacters)textRun;
@ -187,14 +187,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var clusters = textLine.TextRuns.Cast<ShapedTextCharacters>().SelectMany(x => x.ShapedBuffer.GlyphClusters)
.ToArray();
var clusters = BuildGlyphClusters(textLine);
var nextCharacterHit = new CharacterHit(0);
for (var i = 0; i < clusters.Length; i++)
for (var i = 0; i < clusters.Count; i++)
{
Assert.Equal(clusters[i], nextCharacterHit.FirstCharacterIndex);
var expectedCluster = clusters[i];
var actualCluster = nextCharacterHit.FirstCharacterIndex;
Assert.Equal(expectedCluster, actualCluster);
nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit);
}
@ -406,7 +408,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.True(collapsedLine.HasCollapsed);
var trimmedText = collapsedLine.TextRuns.SelectMany(x => x.Text).ToArray();
var trimmedText = collapsedLine.TextRuns.SelectMany(x => new CharacterBufferRange(x)).ToArray();
Assert.Equal(expected.Length, trimmedText.Length);
@ -450,8 +452,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
currentHit = textLine.GetNextCaretCharacterHit(currentHit);
Assert.Equal(3, currentHit.FirstCharacterIndex);
Assert.Equal(1, currentHit.TrailingLength);
Assert.Equal(4, currentHit.FirstCharacterIndex);
Assert.Equal(0, currentHit.TrailingLength);
}
}
@ -473,18 +475,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1));
Assert.Equal(3, currentHit.FirstCharacterIndex);
Assert.Equal(0, currentHit.TrailingLength);
Assert.Equal(2, currentHit.FirstCharacterIndex);
Assert.Equal(1, currentHit.TrailingLength);
currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
Assert.Equal(2, currentHit.FirstCharacterIndex);
Assert.Equal(0, currentHit.TrailingLength);
Assert.Equal(1, currentHit.FirstCharacterIndex);
Assert.Equal(1, currentHit.TrailingLength);
currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
Assert.Equal(1, currentHit.FirstCharacterIndex);
Assert.Equal(0, currentHit.TrailingLength);
Assert.Equal(0, currentHit.FirstCharacterIndex);
Assert.Equal(1, currentHit.TrailingLength);
currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
@ -509,13 +511,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var characterHit = textLine.GetCharacterHitFromDistance(50);
Assert.Equal(3, characterHit.FirstCharacterIndex);
Assert.Equal(5, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetCharacterHitFromDistance(32);
Assert.Equal(2, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
Assert.Equal(3, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
}
}
@ -649,7 +651,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var run = textRuns[i];
var bounds = runBounds[i];
Assert.Equal(run.Text.Start, bounds.TextSourceCharacterIndex);
Assert.Equal(run.CharacterBufferReference.OffsetToFirstChar, bounds.TextSourceCharacterIndex);
Assert.Equal(run, bounds.TextRun);
Assert.Equal(run.Size.Width, bounds.Rectangle.Width);
}
@ -683,13 +685,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
switch (textSourceIndex)
{
case 0:
return new TextCharacters(new ReadOnlySlice<char>("aaaaaaaaaa".AsMemory()), new GenericTextRunProperties(Typeface.Default));
return new TextCharacters("aaaaaaaaaa", new GenericTextRunProperties(Typeface.Default));
case 10:
return new TextCharacters(new ReadOnlySlice<char>("bbbbbbbbbb".AsMemory()), new GenericTextRunProperties(Typeface.Default));
return new TextCharacters("bbbbbbbbbb", new GenericTextRunProperties(Typeface.Default));
case 20:
return new TextCharacters(new ReadOnlySlice<char>("cccccccccc".AsMemory()), new GenericTextRunProperties(Typeface.Default));
return new TextCharacters("cccccccccc", new GenericTextRunProperties(Typeface.Default));
case 30:
return new TextCharacters(new ReadOnlySlice<char>("dddddddddd".AsMemory()), new GenericTextRunProperties(Typeface.Default));
return new TextCharacters("dddddddddd", new GenericTextRunProperties(Typeface.Default));
default:
return null;
}
@ -698,7 +700,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
private class DrawableRunTextSource : ITextSource
{
const string Text = "_A_A";
private const string Text = "_A_A";
public TextRun GetTextRun(int textSourceIndex)
{
@ -707,11 +709,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
case 0:
return new CustomDrawableRun();
case 1:
return new TextCharacters(new ReadOnlySlice<char>(Text.AsMemory(), 1, 1, 1), new GenericTextRunProperties(Typeface.Default));
case 2:
return new TextCharacters(Text, new GenericTextRunProperties(Typeface.Default));
case 5:
return new CustomDrawableRun();
case 3:
return new TextCharacters(new ReadOnlySlice<char>(Text.AsMemory(), 3, 1, 3), new GenericTextRunProperties(Typeface.Default));
case 6:
return new TextCharacters(Text, new GenericTextRunProperties(Typeface.Default));
default:
return null;
}
@ -815,19 +817,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var text = "0123".AsMemory();
var text = "0123";
var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture);
var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, 1, text.Length), shaperOption), defaultProperties);
var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties);
var textRuns = new List<TextRun>
{
new CustomDrawableRun(),
firstRun,
new CustomDrawableRun(),
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length + 2, text.Length), shaperOption), defaultProperties),
new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties),
new CustomDrawableRun(),
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length * 2 + 3, text.Length), shaperOption), defaultProperties)
new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties)
};
var textSource = new FixedRunsTextSource(textRuns);
@ -838,7 +840,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3);
var textBounds = textLine.GetTextBounds(0, textLine.Length);
Assert.Equal(6, textBounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
@ -848,17 +850,17 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(1, textBounds.Count);
Assert.Equal(14, textBounds[0].Rectangle.Width);
textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1);
textBounds = textLine.GetTextBounds(0, firstRun.Length + 1);
Assert.Equal(2, textBounds.Count);
Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(1, firstRun.Text.Length);
textBounds = textLine.GetTextBounds(1, firstRun.Length);
Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width);
textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1);
textBounds = textLine.GetTextBounds(0, 1 + firstRun.Length);
Assert.Equal(2, textBounds.Count);
Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
@ -878,7 +880,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, 0, 200,
new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left,
new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left,
true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0));
var textBounds = textLine.GetTextBounds(0, 3);
@ -899,11 +901,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(2, textBounds.Count);
Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width);
Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width);
Assert.Equal(7.201171875, textBounds[1].Rectangle.Width);
Assert.Equal(firstRun.Size.Width, textBounds[1].Rectangle.Left);
Assert.Equal(firstRun.Size.Width, textBounds[1].Rectangle.Left);
textBounds = textLine.GetTextBounds(0, text.Length);
@ -925,7 +927,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, 0, 200,
new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left,
new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left,
true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0));
var textBounds = textLine.GetTextBounds(0, 4);
@ -941,13 +943,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(1, textBounds.Count);
Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x=> x.Length));
Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x => x.Length));
Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 5);
Assert.Equal(2, textBounds.Count);
Assert.Equal(5, textBounds.Sum(x=> x.TextRunBounds.Sum(x => x.Length)));
Assert.Equal(5, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length)));
Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width);
Assert.Equal(7.201171875, textBounds[0].Rectangle.Width);
@ -960,7 +962,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length)));
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
}
}
}
private class FixedRunsTextSource : ITextSource
{
@ -982,7 +984,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
return textRun;
}
currentPosition += textRun.TextSourceLength;
currentPosition += textRun.Length;
}
return null;

6
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs

@ -14,11 +14,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
using (Start())
{
var text = "\n\r\n".AsMemory();
var text = "\n\r\n";
var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12,0, CultureInfo.CurrentCulture);
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
Assert.Equal(shapedBuffer.Text.Length, text.Length);
Assert.Equal(shapedBuffer.CharacterBufferRange.Length, text.Length);
Assert.Equal(shapedBuffer.GlyphClusters.Count, text.Length);
Assert.Equal(0, shapedBuffer.GlyphClusters[0]);
Assert.Equal(1, shapedBuffer.GlyphClusters[1]);
@ -31,7 +31,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
using (Start())
{
var text = "\t".AsMemory();
var text = "\t";
var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12, 0, CultureInfo.CurrentCulture, 100);
var shapedBuffer = TextShaper.Current.ShapeText(text, options);

8
tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs

@ -11,7 +11,7 @@ namespace Avalonia.UnitTests
{
public class HarfBuzzTextShaperImpl : ITextShaperImpl
{
public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
public ShapedBuffer ShapeText(CharacterBufferReference text, int textLength, TextShaperOptions options)
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
@ -20,7 +20,7 @@ namespace Avalonia.UnitTests
using (var buffer = new Buffer())
{
buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length);
buffer.AddUtf16(text.CharacterBuffer.Span, text.OffsetToFirstChar, textLength);
MergeBreakPair(buffer);
@ -45,7 +45,9 @@ namespace Avalonia.UnitTests
var bufferLength = buffer.Length;
var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
var characterBufferRange = new CharacterBufferRange(text, textLength);
var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
var glyphInfos = buffer.GetGlyphInfoSpan();

12
tests/Avalonia.UnitTests/MockTextShaperImpl.cs

@ -1,24 +1,24 @@
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.UnitTests
{
public class MockTextShaperImpl : ITextShaperImpl
{
public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options)
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
var characterBufferRange = new CharacterBufferRange(text, length);
var shapedBuffer = new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel);
for (var i = 0; i < shapedBuffer.Length;)
{
var glyphCluster = i + text.Start;
var codepoint = Codepoint.ReadAt(text, i, out var count);
var glyphCluster = i + text.OffsetToFirstChar;
var codepoint = Codepoint.ReadAt(characterBufferRange, i, out var count);
var glyphIndex = typeface.GetGlyph(codepoint);

Loading…
Cancel
Save