diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj
index 4a67191132..35a453ce59 100644
--- a/src/Avalonia.Base/Avalonia.Base.csproj
+++ b/src/Avalonia.Base/Avalonia.Base.csproj
@@ -30,6 +30,7 @@
+
diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs
index e82d5b7ba5..2dabb29e76 100644
--- a/src/Avalonia.Base/Media/FontManager.cs
+++ b/src/Avalonia.Base/Media/FontManager.cs
@@ -132,7 +132,7 @@ namespace Avalonia.Media
{
typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
- var glyphTypeface = typeface.GlyphTypeface;
+ var glyphTypeface = GetOrAddGlyphTypeface(typeface);
if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){
return true;
diff --git a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs
index eb42f6443b..f2350f5aea 100644
--- a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs
+++ b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs
@@ -1,13 +1,14 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using System.Text;
using Avalonia.Utilities;
namespace Avalonia.Media.Fonts
{
public sealed class FamilyNameCollection : IReadOnlyList
{
+ private readonly string[] _names;
+
///
/// Initializes a new instance of the class.
///
@@ -20,13 +21,20 @@ namespace Avalonia.Media.Fonts
throw new ArgumentNullException(nameof(familyNames));
}
- Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim());
+ _names = SplitNames(familyNames);
- PrimaryFamilyName = Names[0];
+ PrimaryFamilyName = _names[0];
- HasFallbacks = Names.Count > 1;
+ HasFallbacks = _names.Length > 1;
}
+ private static string[] SplitNames(string names)
+#if NET6_0_OR_GREATER
+ => names.Split(',', StringSplitOptions.TrimEntries);
+#else
+ => Array.ConvertAll(names.Split(','), p => p.Trim());
+#endif
+
///
/// Gets the primary family name.
///
@@ -43,14 +51,6 @@ namespace Avalonia.Media.Fonts
///
public bool HasFallbacks { get; }
- ///
- /// Gets the internal collection of names.
- ///
- ///
- /// The names.
- ///
- internal IReadOnlyList Names { get; }
-
///
/// Returns an enumerator for the name collection.
///
@@ -76,23 +76,7 @@ namespace Avalonia.Media.Fonts
/// A that represents this instance.
///
public override string ToString()
- {
- var builder = StringBuilderCache.Acquire();
-
- for (var index = 0; index < Names.Count; index++)
- {
- builder.Append(Names[index]);
-
- if (index == Names.Count - 1)
- {
- break;
- }
-
- builder.Append(", ");
- }
-
- return StringBuilderCache.GetStringAndRelease(builder);
- }
+ => String.Join(", ", _names);
///
/// Returns a hash code for this instance.
@@ -102,7 +86,7 @@ namespace Avalonia.Media.Fonts
///
public override int GetHashCode()
{
- if (Count == 0)
+ if (_names.Length == 0)
{
return 0;
}
@@ -111,9 +95,9 @@ namespace Avalonia.Media.Fonts
{
int hash = 17;
- for (var i = 0; i < Names.Count; i++)
+ for (var i = 0; i < _names.Length; i++)
{
- string name = Names[i];
+ string name = _names[i];
hash = hash * 23 + name.GetHashCode();
}
@@ -145,30 +129,10 @@ namespace Avalonia.Media.Fonts
/// true if the specified is equal to this instance; otherwise, false.
///
public override bool Equals(object? obj)
- {
- if (!(obj is FamilyNameCollection other))
- {
- return false;
- }
-
- if (other.Count != Count)
- {
- return false;
- }
-
- for (int i = 0; i < Count; i++)
- {
- if (Names[i] != other.Names[i])
- {
- return false;
- }
- }
-
- return true;
- }
+ => obj is FamilyNameCollection other && _names.AsSpan().SequenceEqual(other._names);
- public int Count => Names.Count;
+ public int Count => _names.Length;
- public string this[int index] => Names[index];
+ public string this[int index] => _names[index];
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
index 5c28989c7d..2f8c4ad263 100644
--- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
@@ -128,11 +128,9 @@ namespace Avalonia.Media.TextFormatting
var graphemeEnumerator = new GraphemeEnumerator(text);
- while (graphemeEnumerator.MoveNext())
+ while (graphemeEnumerator.MoveNext(out var grapheme))
{
- var grapheme = graphemeEnumerator.Current;
-
- finalLength += grapheme.Text.Length;
+ finalLength += grapheme.Length;
if (finalLength >= length)
{
diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
index 7afb758038..efcd866bbc 100644
--- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
@@ -60,10 +60,8 @@ namespace Avalonia.Media.TextFormatting
var lineBreakEnumerator = new LineBreakEnumerator(text.Span);
- while (lineBreakEnumerator.MoveNext())
+ while (lineBreakEnumerator.MoveNext(out var currentBreak))
{
- var currentBreak = lineBreakEnumerator.Current;
-
if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length)
{
breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure);
diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
index d444a58297..ac196bf7e0 100644
--- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
@@ -14,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
{
ShapedBuffer = shapedBuffer;
Properties = properties;
- TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize);
+ TextMetrics = new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize);
}
public bool IsReversed { get; private set; }
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
index c1f3816e54..82cf3297fd 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
@@ -47,13 +47,13 @@ namespace Avalonia.Media.TextFormatting
///
/// The shapeable text characters.
internal void GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel,
- ref TextRunProperties? previousProperties, RentedList results)
+ FontManager fontManager, ref TextRunProperties? previousProperties, RentedList results)
{
var properties = Properties;
while (!text.IsEmpty)
{
- var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties);
+ var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, fontManager, ref previousProperties);
results.Add(shapeableRun);
@@ -69,37 +69,40 @@ namespace Avalonia.Media.TextFormatting
/// The characters to create text runs from.
/// The default text run properties.
/// The bidi level of the run.
+ /// The font manager to use.
///
/// A list of shapeable text runs.
private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory text,
- TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
+ TextRunProperties defaultProperties, sbyte biDiLevel, FontManager fontManager,
+ ref TextRunProperties? previousProperties)
{
var defaultTypeface = defaultProperties.Typeface;
- var currentTypeface = defaultTypeface;
+ var defaultGlyphTypeface = defaultProperties.CachedGlyphTypeface;
var previousTypeface = previousProperties?.Typeface;
+ var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface;
var textSpan = text.Span;
- if (TryGetShapeableLength(textSpan, currentTypeface, null, out var count, out var script))
+ if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count, out var script))
{
- if (script == Script.Common && previousTypeface is not null)
+ if (script == Script.Common && previousGlyphTypeface is not null)
{
- if (TryGetShapeableLength(textSpan, previousTypeface.Value, null, out var fallbackCount, out _))
+ if (TryGetShapeableLength(textSpan, previousGlyphTypeface, null, out var fallbackCount, out _))
{
return new UnshapedTextRun(text.Slice(0, fallbackCount),
- defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
+ defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel);
}
}
- return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface),
+ return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface),
biDiLevel);
}
- if (previousTypeface is not null)
+ if (previousGlyphTypeface is not null)
{
- if (TryGetShapeableLength(textSpan, previousTypeface.Value, defaultTypeface, out count, out _))
+ if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count, out _))
{
return new UnshapedTextRun(text.Slice(0, count),
- defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
+ defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel);
}
}
@@ -107,48 +110,44 @@ namespace Avalonia.Media.TextFormatting
var codepointEnumerator = new CodepointEnumerator(text.Slice(count).Span);
- while (codepointEnumerator.MoveNext())
+ while (codepointEnumerator.MoveNext(out var cp))
{
- if (codepointEnumerator.Current.IsWhiteSpace)
+ if (cp.IsWhiteSpace)
{
continue;
}
- codepoint = codepointEnumerator.Current;
+ codepoint = cp;
break;
}
//ToDo: Fix FontFamily fallback
var matchFound =
- FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
+ fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
- out currentTypeface);
+ out var fallbackTypeface);
- if (matchFound && TryGetShapeableLength(textSpan, currentTypeface, defaultTypeface, out count, out _))
+ var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface);
+
+ if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count, out _))
{
//Fallback found
- return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface),
+ return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
biDiLevel);
}
// no fallback found
- currentTypeface = defaultTypeface;
-
- var glyphTypeface = currentTypeface.GlyphTypeface;
-
var enumerator = new GraphemeEnumerator(textSpan);
- while (enumerator.MoveNext())
+ while (enumerator.MoveNext(out var grapheme))
{
- var grapheme = enumerator.Current;
-
- if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
+ if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
{
break;
}
- count += grapheme.Text.Length;
+ count += grapheme.Length;
}
return new UnshapedTextRun(text.Slice(0, count), defaultProperties, biDiLevel);
@@ -158,15 +157,15 @@ namespace Avalonia.Media.TextFormatting
/// Tries to get a shapeable length that is supported by the specified typeface.
///
/// The characters to shape.
- /// The typeface that is used to find matching characters.
- ///
+ /// The typeface that is used to find matching characters.
+ /// The default typeface.
/// The shapeable length.
///
///
internal static bool TryGetShapeableLength(
ReadOnlySpan text,
- Typeface typeface,
- Typeface? defaultTypeface,
+ IGlyphTypeface glyphTypeface,
+ IGlyphTypeface? defaultGlyphTypeface,
out int length,
out Script script)
{
@@ -178,24 +177,22 @@ namespace Avalonia.Media.TextFormatting
return false;
}
- var font = typeface.GlyphTypeface;
- var defaultFont = defaultTypeface?.GlyphTypeface;
-
var enumerator = new GraphemeEnumerator(text);
- while (enumerator.MoveNext())
+ while (enumerator.MoveNext(out var currentGrapheme))
{
- var currentGrapheme = enumerator.Current;
-
- var currentScript = currentGrapheme.FirstCodepoint.Script;
+ var currentCodepoint = currentGrapheme.FirstCodepoint;
+ var currentScript = currentCodepoint.Script;
- if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
+ if (!currentCodepoint.IsWhiteSpace
+ && defaultGlyphTypeface != null
+ && defaultGlyphTypeface.TryGetGlyph(currentCodepoint, out _))
{
break;
}
//Stop at the first missing glyph
- if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
+ if (!currentCodepoint.IsBreakChar && !glyphTypeface.TryGetGlyph(currentCodepoint, out _))
{
break;
}
@@ -216,7 +213,7 @@ namespace Avalonia.Media.TextFormatting
}
}
- length += currentGrapheme.Text.Length;
+ length += currentGrapheme.Length;
}
return length > 0;
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
index e6743f5533..47973e37b5 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
@@ -48,9 +48,9 @@ namespace Avalonia.Media.TextFormatting
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
- while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
+ while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
{
- var nextBreakPosition = lineBreaker.Current.PositionMeasure;
+ var nextBreakPosition = lineBreak.PositionMeasure;
if (nextBreakPosition == 0)
{
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
index bf9f6f77f8..7de842ab39 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
@@ -27,6 +27,7 @@ namespace Avalonia.Media.TextFormatting
TextLineBreak? nextLineBreak = null;
IReadOnlyList? textRuns;
var objectPool = FormattingObjectPool.Instance;
+ var fontManager = FontManager.Current;
var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool,
out var textEndOfLine, out var textSourceLength);
@@ -42,7 +43,7 @@ namespace Avalonia.Media.TextFormatting
}
else
{
- shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, out resolvedFlowDirection);
+ shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, out resolvedFlowDirection);
textRuns = shapedTextRuns;
if (nextLineBreak == null && textEndOfLine != null)
@@ -72,7 +73,7 @@ namespace Avalonia.Media.TextFormatting
case TextWrapping.Wrap:
{
textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth,
- paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool);
+ paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager);
break;
}
default:
@@ -178,12 +179,13 @@ namespace Avalonia.Media.TextFormatting
/// The default paragraph properties.
/// The resolved flow direction.
/// A pool used to get reusable formatting objects.
+ /// The font manager to use.
///
/// A list of shaped text characters.
///
private static RentedList ShapeTextRuns(IReadOnlyList textRuns,
TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool,
- out FlowDirection resolvedFlowDirection)
+ FontManager fontManager, out FlowDirection resolvedFlowDirection)
{
var flowDirection = paragraphProperties.FlowDirection;
var shapedRuns = objectPool.TextRunLists.Rent();
@@ -223,12 +225,13 @@ namespace Avalonia.Media.TextFormatting
var processedRuns = objectPool.TextRunLists.Rent();
- CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns);
+ CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns);
bidiData.Reset();
bidiAlgorithm.Reset();
var groupedRuns = objectPool.UnshapedTextRunLists.Rent();
+ var textShaper = TextShaper.Current;
for (var index = 0; index < processedRuns.Count; index++)
{
@@ -240,7 +243,9 @@ namespace Avalonia.Media.TextFormatting
{
groupedRuns.Clear();
groupedRuns.Add(shapeableRun);
+
var text = shapeableRun.Text;
+ var properties = shapeableRun.Properties;
while (index + 1 < processedRuns.Count)
{
@@ -251,7 +256,7 @@ namespace Avalonia.Media.TextFormatting
if (shapeableRun.BidiLevel == nextRun.BidiLevel
&& TryJoinContiguousMemories(text, nextRun.Text, out var joinedText)
- && CanShapeTogether(shapeableRun.Properties, nextRun.Properties))
+ && CanShapeTogether(properties, nextRun.Properties))
{
groupedRuns.Add(nextRun);
index++;
@@ -263,12 +268,12 @@ namespace Avalonia.Media.TextFormatting
break;
}
- var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
- currentRun.Properties.FontRenderingEmSize,
- shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
- paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
+ var shaperOptions = new TextShaperOptions(
+ properties.CachedGlyphTypeface,
+ properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo,
+ paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
- ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns);
+ ShapeTogether(groupedRuns, text, shaperOptions, textShaper, shapedRuns);
break;
}
@@ -356,9 +361,9 @@ namespace Avalonia.Media.TextFormatting
&& x.BaselineAlignment == y.BaselineAlignment;
private static void ShapeTogether(IReadOnlyList textRuns, ReadOnlyMemory text,
- TextShaperOptions options, RentedList results)
+ TextShaperOptions options, TextShaper textShaper, RentedList results)
{
- var shapedBuffer = TextShaper.Current.ShapeText(text, options);
+ var shapedBuffer = textShaper.ShapeText(text, options);
for (var i = 0; i < textRuns.Count; i++)
{
@@ -377,10 +382,11 @@ namespace Avalonia.Media.TextFormatting
///
/// The text characters to form from.
/// The bidi levels.
+ /// The font manager to use.
/// A list that will be filled with the processed runs.
///
private static void CoalesceLevels(IReadOnlyList textCharacters, ReadOnlySpan levels,
- RentedList processedRuns)
+ FontManager fontManager, RentedList processedRuns)
{
if (levels.Length == 0)
{
@@ -427,8 +433,8 @@ namespace Avalonia.Media.TextFormatting
if (j == runTextSpan.Length)
{
- currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties,
- processedRuns);
+ currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, fontManager,
+ ref previousProperties, processedRuns);
runLevel = levels[levelIndex];
@@ -441,8 +447,8 @@ namespace Avalonia.Media.TextFormatting
}
// End of this run
- currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties,
- processedRuns);
+ currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, fontManager,
+ ref previousProperties, processedRuns);
runText = runText.Slice(j);
runTextSpan = runText.Span;
@@ -459,7 +465,7 @@ namespace Avalonia.Media.TextFormatting
return;
}
- currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties, processedRuns);
+ currentRun.GetShapeableCharacters(runText, runLevel, fontManager, ref previousProperties, processedRuns);
}
///
@@ -554,15 +560,13 @@ namespace Avalonia.Media.TextFormatting
var lineBreakEnumerator = new LineBreakEnumerator(text.Span);
- while (lineBreakEnumerator.MoveNext())
+ while (lineBreakEnumerator.MoveNext(out lineBreak))
{
- if (!lineBreakEnumerator.Current.Required)
+ if (!lineBreak.Required)
{
continue;
}
- lineBreak = lineBreakEnumerator.Current;
-
return lineBreak.PositionWrap >= textRun.Length || true;
}
@@ -637,11 +641,11 @@ namespace Avalonia.Media.TextFormatting
///
/// The empty text line.
public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth,
- TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool)
+ TextParagraphProperties paragraphProperties, FontManager fontManager)
{
var flowDirection = paragraphProperties.FlowDirection;
var properties = paragraphProperties.DefaultTextRunProperties;
- var glyphTypeface = properties.Typeface.GlyphTypeface;
+ var glyphTypeface = properties.CachedGlyphTypeface;
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) };
@@ -665,14 +669,15 @@ namespace Avalonia.Media.TextFormatting
///
/// The current line break if the line was explicitly broken.
/// A pool used to get reusable formatting objects.
+ /// The font manager to use.
/// The wrapped text line.
private static TextLineImpl PerformTextWrapping(IReadOnlyList textRuns, int firstTextSourceIndex,
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection,
- TextLineBreak? currentLineBreak, FormattingObjectPool objectPool)
+ TextLineBreak? currentLineBreak, FormattingObjectPool objectPool, FontManager fontManager)
{
if (textRuns.Count == 0)
{
- return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, objectPool);
+ return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, fontManager);
}
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
@@ -698,20 +703,20 @@ namespace Avalonia.Media.TextFormatting
{
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
- while (lineBreaker.MoveNext())
+ while (lineBreaker.MoveNext(out var lineBreak))
{
- if (lineBreaker.Current.Required &&
- currentLength + lineBreaker.Current.PositionMeasure <= measuredLength)
+ if (lineBreak.Required &&
+ currentLength + lineBreak.PositionMeasure <= measuredLength)
{
//Explicit break found
breakFound = true;
- currentPosition = currentLength + lineBreaker.Current.PositionWrap;
+ currentPosition = currentLength + lineBreak.PositionWrap;
break;
}
- if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength)
+ if (currentLength + lineBreak.PositionMeasure > measuredLength)
{
if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
{
@@ -727,21 +732,21 @@ namespace Avalonia.Media.TextFormatting
//Find next possible wrap position (overflow)
if (index < textRuns.Count - 1)
{
- if (lineBreaker.Current.PositionWrap != currentRun.Length)
+ if (lineBreak.PositionWrap != currentRun.Length)
{
//We already found the next possible wrap position.
breakFound = true;
- currentPosition = currentLength + lineBreaker.Current.PositionWrap;
+ currentPosition = currentLength + lineBreak.PositionWrap;
break;
}
- while (lineBreaker.MoveNext() && index < textRuns.Count)
+ while (lineBreaker.MoveNext(out lineBreak) && index < textRuns.Count)
{
- currentPosition += lineBreaker.Current.PositionWrap;
+ currentPosition += lineBreak.PositionWrap;
- if (lineBreaker.Current.PositionWrap != currentRun.Length)
+ if (lineBreak.PositionWrap != currentRun.Length)
{
break;
}
@@ -760,7 +765,7 @@ namespace Avalonia.Media.TextFormatting
}
else
{
- currentPosition = currentLength + lineBreaker.Current.PositionWrap;
+ currentPosition = currentLength + lineBreak.PositionWrap;
}
breakFound = true;
@@ -776,9 +781,9 @@ namespace Avalonia.Media.TextFormatting
break;
}
- if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap)
+ if (lineBreak.PositionMeasure != lineBreak.PositionWrap)
{
- lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap;
+ lastWrapPosition = currentLength + lineBreak.PositionWrap;
}
}
@@ -800,18 +805,18 @@ namespace Avalonia.Media.TextFormatting
var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool);
- var lineBreak = postSplitRuns?.Count > 0 ?
+ var textLineBreak = postSplitRuns?.Count > 0 ?
new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) :
null;
- if (lineBreak is null && currentLineBreak?.TextEndOfLine != null)
+ if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null)
{
- lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
+ textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
}
var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection,
- lineBreak);
+ textLineBreak);
textLine.FinalizeLine();
@@ -868,7 +873,7 @@ namespace Avalonia.Media.TextFormatting
{
var textShaper = TextShaper.Current;
- var glyphTypeface = textRun.Properties!.Typeface.GlyphTypeface;
+ var glyphTypeface = textRun.Properties!.CachedGlyphTypeface;
var fontRenderingEmSize = textRun.Properties.FontRenderingEmSize;
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
index 7a74dc89ae..bb58e0d692 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
@@ -427,11 +427,12 @@ namespace Avalonia.Media.TextFormatting
private TextLine[] CreateTextLines()
{
var objectPool = FormattingObjectPool.Instance;
+ var fontManager = FontManager.Current;
if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties,
- FormattingObjectPool.Instance);
+ fontManager);
Bounds = new Rect(0, 0, 0, textLine.Height);
@@ -458,7 +459,7 @@ namespace Avalonia.Media.TextFormatting
if (previousLine != null && previousLine.NewLineLength > 0)
{
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth,
- _paragraphProperties, objectPool);
+ _paragraphProperties, fontManager);
textLines.Add(emptyTextLine);
@@ -517,7 +518,7 @@ namespace Avalonia.Media.TextFormatting
//Make sure the TextLayout always contains at least on empty line
if (textLines.Count == 0)
{
- var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, objectPool);
+ var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager);
textLines.Add(textLine);
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
index 260fcaccbe..ad3244a3a5 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
@@ -1256,7 +1256,7 @@ namespace Avalonia.Media.TextFormatting
private TextLineMetrics CreateLineMetrics()
{
- var fontMetrics = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface.Metrics;
+ var fontMetrics = _paragraphProperties.DefaultTextRunProperties.CachedGlyphTypeface.Metrics;
var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize;
var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight;
@@ -1285,12 +1285,13 @@ namespace Avalonia.Media.TextFormatting
{
case ShapedTextRun textRun:
{
+ var properties = textRun.Properties;
var textMetrics =
- new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize);
+ new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize);
- if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize)
+ if (fontRenderingEmSize < properties.FontRenderingEmSize)
{
- fontRenderingEmSize = textRun.Properties.FontRenderingEmSize;
+ fontRenderingEmSize = properties.FontRenderingEmSize;
if (ascent > textMetrics.Ascent)
{
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs
index 7bad99f33f..1622bc3b6d 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs
@@ -12,6 +12,8 @@ namespace Avalonia.Media.TextFormatting
///
public abstract class TextRunProperties : IEquatable
{
+ private IGlyphTypeface? _cachedGlyphTypeFace;
+
///
/// Run typeface
///
@@ -47,6 +49,9 @@ namespace Avalonia.Media.TextFormatting
///
public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline;
+ internal IGlyphTypeface CachedGlyphTypeface
+ => _cachedGlyphTypeFace ??= Typeface.GlyphTypeface;
+
public bool Equals(TextRunProperties? other)
{
if (ReferenceEquals(null, other))
diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
index 3a81784152..3406432ce7 100644
--- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
@@ -343,6 +343,17 @@ namespace Avalonia.Media.TextFormatting.Unicode
return 0;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsIsolateStart(BidiClass type)
+ {
+ const uint mask =
+ (1U << (int)BidiClass.LeftToRightIsolate) |
+ (1U << (int)BidiClass.RightToLeftIsolate) |
+ (1U << (int)BidiClass.FirstStrongIsolate);
+
+ return ((1U << (int)type) & mask) != 0U;
+ }
+
///
/// Build a list of matching isolates for a directionality slice
/// Implements BD9
@@ -701,28 +712,19 @@ namespace Avalonia.Media.TextFormatting.Unicode
var lastType = _workingClasses[lastCharIndex];
int nextLevel;
- switch (lastType)
+ if (IsIsolateStart(lastType))
{
- case BidiClass.LeftToRightIsolate:
- case BidiClass.RightToLeftIsolate:
- case BidiClass.FirstStrongIsolate:
+ nextLevel = _paragraphEmbeddingLevel;
+ }
+ else
+ {
+ i = lastCharIndex + 1;
+ while (i < _originalClasses.Length && IsRemovedByX9(_originalClasses[i]))
{
- nextLevel = _paragraphEmbeddingLevel;
-
- break;
+ i++;
}
- default:
- {
- i = lastCharIndex + 1;
- while (i < _originalClasses.Length && IsRemovedByX9(_originalClasses[i]))
- {
- i++;
- }
- nextLevel = i >= _originalClasses.Length ? _paragraphEmbeddingLevel : _resolvedLevels[i];
-
- break;
- }
+ nextLevel = i >= _originalClasses.Length ? _paragraphEmbeddingLevel : _resolvedLevels[i];
}
var eos = DirectionFromLevel(Math.Max(nextLevel, level));
@@ -831,8 +833,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
// PDI and concatenate that run to this one
var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1];
var lastType = _originalClasses[lastCharacterIndex];
- if ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) &&
- _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex))
+ if (IsIsolateStart(lastType) && _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex))
{
// Find the continuing run index
runIndex = FindRunForIndex(nextRunIndex);
@@ -869,74 +870,59 @@ namespace Avalonia.Media.TextFormatting.Unicode
_runDirection = DirectionFromLevel(runLevel);
_runLength = _runResolvedClasses.Length;
- // By tracking the types of characters known to be in the current run, we can
- // skip some of the rules that we know won't apply. The flags will be
- // initialized while we're processing rule W1 below.
- var hasEN = false;
- var hasAL = false;
- var hasES = false;
- var hasCS = false;
- var hasAN = false;
- var hasET = false;
-
// Rule W1
// Also, set hasXX flags
int i;
var previousClass = sos;
+ const uint isolateMask =
+ (1U << (int)BidiClass.LeftToRightIsolate) |
+ (1U << (int)BidiClass.RightToLeftIsolate) |
+ (1U << (int)BidiClass.FirstStrongIsolate) |
+ (1U << (int)BidiClass.PopDirectionalIsolate);
+
+ const uint wRulesMask =
+ (1U << (int)BidiClass.EuropeanNumber) |
+ (1U << (int)BidiClass.ArabicLetter) |
+ (1U << (int)BidiClass.EuropeanSeparator) |
+ (1U << (int)BidiClass.CommonSeparator) |
+ (1U << (int)BidiClass.ArabicNumber) |
+ (1U << (int)BidiClass.EuropeanTerminator);
+
+ uint wRules = 0;
+
for (i = 0; i < _runLength; i++)
{
var resolvedClass = _runResolvedClasses[i];
-
- switch (resolvedClass)
- {
- case BidiClass.NonspacingMark:
- _runResolvedClasses[i] = previousClass;
- break;
- case BidiClass.LeftToRightIsolate:
- case BidiClass.RightToLeftIsolate:
- case BidiClass.FirstStrongIsolate:
- case BidiClass.PopDirectionalIsolate:
+ if (resolvedClass == BidiClass.NonspacingMark)
+ {
+ _runResolvedClasses[i] = previousClass;
+ }
+ else
+ {
+ var classBit = 1U << (int)resolvedClass;
+ if ((classBit & isolateMask) != 0U)
+ {
previousClass = BidiClass.OtherNeutral;
- break;
-
- case BidiClass.EuropeanNumber:
- hasEN = true;
- previousClass = resolvedClass;
- break;
-
- case BidiClass.ArabicLetter:
- hasAL = true;
- previousClass = resolvedClass;
- break;
-
- case BidiClass.EuropeanSeparator:
- hasES = true;
- previousClass = resolvedClass;
- break;
-
- case BidiClass.CommonSeparator:
- hasCS = true;
- previousClass = resolvedClass;
- break;
-
- case BidiClass.ArabicNumber:
- hasAN = true;
- previousClass = resolvedClass;
- break;
-
- case BidiClass.EuropeanTerminator:
- hasET = true;
- previousClass = resolvedClass;
- break;
-
- default:
+ }
+ else
+ {
+ wRules |= classBit & wRulesMask;
previousClass = resolvedClass;
- break;
+ }
}
}
+ // By tracking the types of characters known to be in the current run, we can
+ // skip some of the rules that we know won't apply.
+ var hasEN = (wRules & (1U << (int)BidiClass.EuropeanNumber)) != 0U;
+ var hasAL = (wRules & (1U << (int)BidiClass.ArabicLetter)) != 0U;
+ var hasES = (wRules & (1U << (int)BidiClass.EuropeanSeparator)) != 0U;
+ var hasCS = (wRules & (1U << (int)BidiClass.CommonSeparator)) != 0U;
+ var hasAN = (wRules & (1U << (int)BidiClass.ArabicNumber)) != 0U;
+ var hasET = (wRules & (1U << (int)BidiClass.EuropeanTerminator)) != 0U;
+
// Rule W2
if (hasEN)
{
@@ -1548,23 +1534,20 @@ namespace Avalonia.Media.TextFormatting.Unicode
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsWhitespace(BidiClass biDiClass)
{
- switch (biDiClass)
- {
- case BidiClass.LeftToRightEmbedding:
- case BidiClass.RightToLeftEmbedding:
- case BidiClass.LeftToRightOverride:
- case BidiClass.RightToLeftOverride:
- case BidiClass.PopDirectionalFormat:
- case BidiClass.LeftToRightIsolate:
- case BidiClass.RightToLeftIsolate:
- case BidiClass.FirstStrongIsolate:
- case BidiClass.PopDirectionalIsolate:
- case BidiClass.BoundaryNeutral:
- case BidiClass.WhiteSpace:
- return true;
- default:
- return false;
- }
+ const uint mask =
+ (1U << (int)BidiClass.LeftToRightEmbedding) |
+ (1U << (int)BidiClass.RightToLeftEmbedding) |
+ (1U << (int)BidiClass.LeftToRightOverride) |
+ (1U << (int)BidiClass.RightToLeftOverride) |
+ (1U << (int)BidiClass.PopDirectionalFormat) |
+ (1U << (int)BidiClass.LeftToRightIsolate) |
+ (1U << (int)BidiClass.RightToLeftIsolate) |
+ (1U << (int)BidiClass.FirstStrongIsolate) |
+ (1U << (int)BidiClass.PopDirectionalIsolate) |
+ (1U << (int)BidiClass.BoundaryNeutral) |
+ (1U << (int)BidiClass.WhiteSpace);
+
+ return ((1U << (int)biDiClass) & mask) != 0U;
}
///
@@ -1585,18 +1568,15 @@ namespace Avalonia.Media.TextFormatting.Unicode
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsRemovedByX9(BidiClass biDiClass)
{
- switch (biDiClass)
- {
- case BidiClass.LeftToRightEmbedding:
- case BidiClass.RightToLeftEmbedding:
- case BidiClass.LeftToRightOverride:
- case BidiClass.RightToLeftOverride:
- case BidiClass.PopDirectionalFormat:
- case BidiClass.BoundaryNeutral:
- return true;
- default:
- return false;
- }
+ const uint mask =
+ (1U << (int)BidiClass.LeftToRightEmbedding) |
+ (1U << (int)BidiClass.RightToLeftEmbedding) |
+ (1U << (int)BidiClass.LeftToRightOverride) |
+ (1U << (int)BidiClass.RightToLeftOverride) |
+ (1U << (int)BidiClass.PopDirectionalFormat) |
+ (1U << (int)BidiClass.BoundaryNeutral);
+
+ return ((1U << (int)biDiClass) & mask) != 0U;
}
///
@@ -1605,20 +1585,17 @@ namespace Avalonia.Media.TextFormatting.Unicode
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsNeutralClass(BidiClass direction)
{
- switch (direction)
- {
- case BidiClass.ParagraphSeparator:
- case BidiClass.SegmentSeparator:
- case BidiClass.WhiteSpace:
- case BidiClass.OtherNeutral:
- case BidiClass.RightToLeftIsolate:
- case BidiClass.LeftToRightIsolate:
- case BidiClass.FirstStrongIsolate:
- case BidiClass.PopDirectionalIsolate:
- return true;
- default:
- return false;
- }
+ const uint mask =
+ (1U << (int)BidiClass.ParagraphSeparator) |
+ (1U << (int)BidiClass.SegmentSeparator) |
+ (1U << (int)BidiClass.WhiteSpace) |
+ (1U << (int)BidiClass.OtherNeutral) |
+ (1U << (int)BidiClass.RightToLeftIsolate) |
+ (1U << (int)BidiClass.LeftToRightIsolate) |
+ (1U << (int)BidiClass.FirstStrongIsolate) |
+ (1U << (int)BidiClass.PopDirectionalIsolate);
+
+ return ((1U << (int)direction) & mask) != 0U;
}
///
diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
index 5cc222b813..b8094056f2 100644
--- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
@@ -73,39 +73,32 @@ namespace Avalonia.Media.TextFormatting.Unicode
// bracket values for all code points
int i = Length;
+
+ const uint embeddingMask =
+ (1U << (int)BidiClass.LeftToRightEmbedding) |
+ (1U << (int)BidiClass.LeftToRightOverride) |
+ (1U << (int)BidiClass.RightToLeftEmbedding) |
+ (1U << (int)BidiClass.RightToLeftOverride) |
+ (1U << (int)BidiClass.PopDirectionalFormat);
+
+ const uint isolateMask =
+ (1U << (int)BidiClass.LeftToRightIsolate) |
+ (1U << (int)BidiClass.RightToLeftIsolate) |
+ (1U << (int)BidiClass.FirstStrongIsolate) |
+ (1U << (int)BidiClass.PopDirectionalIsolate);
var codePointEnumerator = new CodepointEnumerator(text);
- while (codePointEnumerator.MoveNext())
+ while (codePointEnumerator.MoveNext(out var codepoint))
{
- var codepoint = codePointEnumerator.Current;
-
// Look up BiDiClass
var dir = codepoint.BiDiClass;
_classes[i] = dir;
- switch (dir)
- {
- case BidiClass.LeftToRightEmbedding:
- case BidiClass.LeftToRightOverride:
- case BidiClass.RightToLeftEmbedding:
- case BidiClass.RightToLeftOverride:
- case BidiClass.PopDirectionalFormat:
- {
- HasEmbeddings = true;
- break;
- }
-
- case BidiClass.LeftToRightIsolate:
- case BidiClass.RightToLeftIsolate:
- case BidiClass.FirstStrongIsolate:
- case BidiClass.PopDirectionalIsolate:
- {
- HasIsolates = true;
- break;
- }
- }
+ var dirBit = 1U << (int)dir;
+ HasEmbeddings = (dirBit & embeddingMask) != 0U;
+ HasIsolates = (dirBit & isolateMask) != 0U;
// Lookup paired bracket types
var pbt = codepoint.PairedBracketType;
diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
index 22f7b50fd4..23a1e4a275 100644
--- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Avalonia.Media.TextFormatting.Unicode
@@ -11,13 +10,19 @@ namespace Avalonia.Media.TextFormatting.Unicode
///
/// The replacement codepoint that is used for non supported values.
///
- public static readonly Codepoint ReplacementCodepoint = new Codepoint('\uFFFD');
-
- public Codepoint(uint value)
+ public static Codepoint ReplacementCodepoint
{
- _value = value;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new('\uFFFD');
}
+ ///
+ /// Creates a new instance of with the specified value.
+ ///
+ /// The codepoint value.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Codepoint(uint value) => _value = value;
+
///
/// Get the codepoint's value.
///
@@ -87,19 +92,17 @@ namespace Avalonia.Media.TextFormatting.Unicode
///
public bool IsWhiteSpace
{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
- switch (GeneralCategory)
- {
- case GeneralCategory.Control:
- case GeneralCategory.NonspacingMark:
- case GeneralCategory.Format:
- case GeneralCategory.SpaceSeparator:
- case GeneralCategory.SpacingMark:
- return true;
- }
-
- return false;
+ const ulong whiteSpaceMask =
+ (1UL << (int)GeneralCategory.Control) |
+ (1UL << (int)GeneralCategory.NonspacingMark) |
+ (1UL << (int)GeneralCategory.Format) |
+ (1UL << (int)GeneralCategory.SpaceSeparator) |
+ (1UL << (int)GeneralCategory.SpacingMark);
+
+ return ((1UL << (int)GeneralCategory) & whiteSpaceMask) != 0UL;
}
}
@@ -166,56 +169,62 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// The index to read at.
/// The count of character that were read.
///
+#if NET6_0_OR_GREATER
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+#else
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+#endif
public static Codepoint ReadAt(ReadOnlySpan text, int index, out int count)
{
+ // Perf note: this method is performance critical for text layout, modify with care!
+
count = 1;
- if (index >= text.Length)
+ // Perf note: uint check allows the JIT to ellide the next bound check
+ if ((uint)index >= (uint)text.Length)
{
return ReplacementCodepoint;
}
- var code = text[index];
-
- ushort hi, low;
+ uint code = text[index];
- //# High surrogate
- if (0xD800 <= code && code <= 0xDBFF)
+ //# Surrogate
+ if (IsInRangeInclusive(code, 0xD800U, 0xDFFFU))
{
- hi = code;
-
- if (index + 1 == text.Length)
- {
- return ReplacementCodepoint;
- }
-
- low = text[index + 1];
-
- if (0xDC00 <= low && low <= 0xDFFF)
- {
- count = 2;
- return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
- }
-
- return ReplacementCodepoint;
- }
+ uint hi, low;
- //# Low surrogate
- if (0xDC00 <= code && code <= 0xDFFF)
- {
- if (index == 0)
+ //# High surrogate
+ if (code <= 0xDBFF)
{
- return ReplacementCodepoint;
+ if ((uint)(index + 1) < (uint)text.Length)
+ {
+ hi = code;
+ low = text[index + 1];
+
+ if (IsInRangeInclusive(low, 0xDC00U, 0xDFFFU))
+ {
+ count = 2;
+ // Perf note: the code is written as below to become just two instructions: shl, lea.
+ // See https://github.com/dotnet/runtime/blob/7ec3634ee579d89b6024f72b595bfd7118093fc5/src/libraries/System.Private.CoreLib/src/System/Text/UnicodeUtility.cs#L38
+ return new Codepoint((hi << 10) + low - ((0xD800U << 10) + 0xDC00U - (1 << 16)));
+ }
+ }
}
- hi = text[index - 1];
-
- low = code;
-
- if (0xD800 <= hi && hi <= 0xDBFF)
+ //# Low surrogate
+ else
{
- count = 2;
- return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
+ if (index > 0)
+ {
+ low = code;
+ hi = text[index - 1];
+
+ if (IsInRangeInclusive(hi, 0xD800U, 0xDBFFU))
+ {
+ count = 2;
+ return new Codepoint((hi << 10) + low - ((0xD800U << 10) + 0xDC00U - (1 << 16)));
+ }
+ }
}
return ReplacementCodepoint;
@@ -224,12 +233,16 @@ namespace Avalonia.Media.TextFormatting.Unicode
return new Codepoint(code);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound)
+ => value - lowerBound <= upperBound - lowerBound;
+
///
/// Returns if is between
/// and , inclusive.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsInRangeInclusive(Codepoint cp, uint lowerBound, uint upperBound)
- => (cp._value - lowerBound) <= (upperBound - lowerBound);
+ => IsInRangeInclusive(cp._value, lowerBound, upperBound);
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs
index d21f30ab7e..47a2b7d46a 100644
--- a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs
@@ -4,35 +4,27 @@ namespace Avalonia.Media.TextFormatting.Unicode
{
public ref struct CodepointEnumerator
{
- private ReadOnlySpan _text;
+ private readonly ReadOnlySpan _text;
+ private int _offset;
public CodepointEnumerator(ReadOnlySpan text)
- {
- _text = text;
- Current = Codepoint.ReplacementCodepoint;
- }
-
- ///
- /// Gets the current .
- ///
- public Codepoint Current { get; private set; }
+ => _text = text;
///
/// Moves to the next .
///
///
- public bool MoveNext()
+ public bool MoveNext(out Codepoint codepoint)
{
- if (_text.IsEmpty)
+ if ((uint)_offset >= (uint)_text.Length)
{
- Current = Codepoint.ReplacementCodepoint;
-
+ codepoint = Codepoint.ReplacementCodepoint;
return false;
}
- Current = Codepoint.ReadAt(_text, 0, out var count);
+ codepoint = Codepoint.ReadAt(_text, _offset, out var count);
- _text = _text.Slice(count);
+ _offset += count;
return true;
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
index fa8e8ac976..fcc12d3526 100644
--- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
@@ -1,16 +1,15 @@
-using System;
-
-namespace Avalonia.Media.TextFormatting.Unicode
+namespace Avalonia.Media.TextFormatting.Unicode
{
///
/// Represents the smallest unit of a writing system of any given language.
///
public readonly ref struct Grapheme
{
- public Grapheme(Codepoint firstCodepoint, ReadOnlySpan text)
+ public Grapheme(Codepoint firstCodepoint, int offset, int length)
{
FirstCodepoint = firstCodepoint;
- Text = text;
+ Offset = offset;
+ Length = length;
}
///
@@ -19,8 +18,13 @@ namespace Avalonia.Media.TextFormatting.Unicode
public Codepoint FirstCodepoint { get; }
///
- /// The text of the grapheme cluster
+ /// Gets the starting code unit offset of this grapheme inside its containing text.
+ ///
+ public int Offset { get; }
+
+ ///
+ /// Gets the length of this grapheme, in code units.
///
- public ReadOnlySpan Text { get; }
+ public int Length { get; }
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
index 812bb99d99..dd01662155 100644
--- a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
@@ -4,57 +4,79 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
-using System.Runtime.InteropServices;
namespace Avalonia.Media.TextFormatting.Unicode
{
public ref struct GraphemeEnumerator
{
- private ReadOnlySpan _text;
+ private readonly ReadOnlySpan _text;
+ private int _currentCodeUnitOffset;
+ private int _codeUnitLengthOfCurrentCodepoint;
+ private Codepoint _currentCodepoint;
+
+ ///
+ /// Will be if invalid data or EOF reached.
+ /// Caller shouldn't need to special-case this since the normal rules will halt on this condition.
+ ///
+ private GraphemeBreakClass _currentType;
public GraphemeEnumerator(ReadOnlySpan text)
{
_text = text;
- Current = default;
+ _currentCodeUnitOffset = 0;
+ _codeUnitLengthOfCurrentCodepoint = 0;
+ _currentCodepoint = Codepoint.ReplacementCodepoint;
+ _currentType = GraphemeBreakClass.Other;
}
- ///
- /// Gets the current .
- ///
- public Grapheme Current { get; private set; }
-
///
/// Moves to the next .
///
///
- public bool MoveNext()
+ public bool MoveNext(out Grapheme grapheme)
{
- if (_text.IsEmpty)
+ var startOffset = _currentCodeUnitOffset;
+
+ if ((uint)startOffset >= (uint)_text.Length)
{
+ grapheme = default;
return false;
}
// Algorithm given at https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules.
- var processor = new Processor(_text);
-
- processor.MoveNext();
+ if (startOffset == 0)
+ {
+ ReadNextCodepoint();
+ }
- var firstCodepoint = processor.CurrentCodepoint;
+ var firstCodepoint = _currentCodepoint;
// First, consume as many Prepend scalars as we can (rule GB9b).
- while (processor.CurrentType == GraphemeBreakClass.Prepend)
+ if (_currentType == GraphemeBreakClass.Prepend)
{
- processor.MoveNext();
+ do
+ {
+ ReadNextCodepoint();
+ } while (_currentType == GraphemeBreakClass.Prepend);
+
+ // There were only Prepend scalars in the text
+ if ((uint)_currentCodeUnitOffset >= (uint)_text.Length)
+ {
+ goto Return;
+ }
}
// Next, make sure we're not about to violate control character restrictions.
// Essentially, if we saw Prepend data, we can't have Control | CR | LF data afterward (rule GB5).
- if (processor.CurrentCodeUnitOffset > 0)
+ if (_currentCodeUnitOffset > startOffset)
{
- if (processor.CurrentType == GraphemeBreakClass.Control
- || processor.CurrentType == GraphemeBreakClass.CR
- || processor.CurrentType == GraphemeBreakClass.LF)
+ const uint controlCrLfMask =
+ (1U << (int)GraphemeBreakClass.Control) |
+ (1U << (int)GraphemeBreakClass.CR) |
+ (1U << (int)GraphemeBreakClass.LF);
+
+ if (((1U << (int)_currentType) & controlCrLfMask) != 0U)
{
goto Return;
}
@@ -62,19 +84,19 @@ namespace Avalonia.Media.TextFormatting.Unicode
// Now begin the main state machine.
- var previousClusterBreakType = processor.CurrentType;
+ var previousClusterBreakType = _currentType;
- processor.MoveNext();
+ ReadNextCodepoint();
switch (previousClusterBreakType)
{
case GraphemeBreakClass.CR:
- if (processor.CurrentType != GraphemeBreakClass.LF)
+ if (_currentType != GraphemeBreakClass.LF)
{
goto Return; // rules GB3 & GB4 (only can follow )
}
- processor.MoveNext();
+ ReadNextCodepoint();
goto case GraphemeBreakClass.LF;
case GraphemeBreakClass.Control:
@@ -82,53 +104,57 @@ namespace Avalonia.Media.TextFormatting.Unicode
goto Return; // rule GB4 (no data after Control | LF)
case GraphemeBreakClass.L:
- if (processor.CurrentType == GraphemeBreakClass.L)
+ {
+ if (_currentType == GraphemeBreakClass.L)
{
- processor.MoveNext(); // rule GB6 (L x L)
+ ReadNextCodepoint(); // rule GB6 (L x L)
goto case GraphemeBreakClass.L;
}
- else if (processor.CurrentType == GraphemeBreakClass.V)
+ else if (_currentType == GraphemeBreakClass.V)
{
- processor.MoveNext(); // rule GB6 (L x V)
+ ReadNextCodepoint(); // rule GB6 (L x V)
goto case GraphemeBreakClass.V;
}
- else if (processor.CurrentType == GraphemeBreakClass.LV)
+ else if (_currentType == GraphemeBreakClass.LV)
{
- processor.MoveNext(); // rule GB6 (L x LV)
+ ReadNextCodepoint(); // rule GB6 (L x LV)
goto case GraphemeBreakClass.LV;
}
- else if (processor.CurrentType == GraphemeBreakClass.LVT)
+ else if (_currentType == GraphemeBreakClass.LVT)
{
- processor.MoveNext(); // rule GB6 (L x LVT)
+ ReadNextCodepoint(); // rule GB6 (L x LVT)
goto case GraphemeBreakClass.LVT;
}
else
{
break;
}
+ }
case GraphemeBreakClass.LV:
case GraphemeBreakClass.V:
- if (processor.CurrentType == GraphemeBreakClass.V)
+ {
+ if (_currentType == GraphemeBreakClass.V)
{
- processor.MoveNext(); // rule GB7 (LV | V x V)
+ ReadNextCodepoint(); // rule GB7 (LV | V x V)
goto case GraphemeBreakClass.V;
}
- else if (processor.CurrentType == GraphemeBreakClass.T)
+ else if (_currentType == GraphemeBreakClass.T)
{
- processor.MoveNext(); // rule GB7 (LV | V x T)
+ ReadNextCodepoint(); // rule GB7 (LV | V x T)
goto case GraphemeBreakClass.T;
}
else
{
break;
}
+ }
case GraphemeBreakClass.LVT:
case GraphemeBreakClass.T:
- if (processor.CurrentType == GraphemeBreakClass.T)
+ if (_currentType == GraphemeBreakClass.T)
{
- processor.MoveNext(); // rule GB8 (LVT | T x T)
+ ReadNextCodepoint(); // rule GB8 (LVT | T x T)
goto case GraphemeBreakClass.T;
}
else
@@ -139,123 +165,79 @@ namespace Avalonia.Media.TextFormatting.Unicode
case GraphemeBreakClass.ExtendedPictographic:
// Attempt processing extended pictographic (rules GB11, GB9).
// First, drain any Extend scalars that might exist
- while (processor.CurrentType == GraphemeBreakClass.Extend)
+ while (_currentType == GraphemeBreakClass.Extend)
{
- processor.MoveNext();
+ ReadNextCodepoint();
}
// Now see if there's a ZWJ + extended pictograph again.
- if (processor.CurrentType != GraphemeBreakClass.ZWJ)
+ if (_currentType != GraphemeBreakClass.ZWJ)
{
break;
}
- processor.MoveNext();
- if (processor.CurrentType != GraphemeBreakClass.ExtendedPictographic)
+ ReadNextCodepoint();
+ if (_currentType != GraphemeBreakClass.ExtendedPictographic)
{
break;
}
- processor.MoveNext();
+ ReadNextCodepoint();
goto case GraphemeBreakClass.ExtendedPictographic;
case GraphemeBreakClass.RegionalIndicator:
// We've consumed a single RI scalar. Try to consume another (to make it a pair).
- if (processor.CurrentType == GraphemeBreakClass.RegionalIndicator)
+ if (_currentType == GraphemeBreakClass.RegionalIndicator)
{
- processor.MoveNext();
+ ReadNextCodepoint();
}
// Standlone RI scalars (or a single pair of RI scalars) can only be followed by trailers.
break; // nothing but trailers after the final RI
-
- default:
- break;
}
+ const uint gb9Mask =
+ (1U << (int)GraphemeBreakClass.Extend) |
+ (1U << (int)GraphemeBreakClass.ZWJ) |
+ (1U << (int)GraphemeBreakClass.SpacingMark);
+
// rules GB9, GB9a
- while (processor.CurrentType == GraphemeBreakClass.Extend
- || processor.CurrentType == GraphemeBreakClass.ZWJ
- || processor.CurrentType == GraphemeBreakClass.SpacingMark)
+ while (((1U << (int)_currentType) & gb9Mask) != 0U)
{
- processor.MoveNext();
+ ReadNextCodepoint();
}
Return:
- Current = new Grapheme(firstCodepoint, _text.Slice(0, processor.CurrentCodeUnitOffset));
-
- _text = _text.Slice(processor.CurrentCodeUnitOffset);
+ var graphemeLength = _currentCodeUnitOffset - startOffset;
+ grapheme = new Grapheme(firstCodepoint, startOffset, graphemeLength);
return true; // rules GB2, GB999
}
- [StructLayout(LayoutKind.Auto)]
- private ref struct Processor
+ private void ReadNextCodepoint()
{
- private readonly ReadOnlySpan _buffer;
- private int _codeUnitLengthOfCurrentScalar;
-
- internal Processor(ReadOnlySpan buffer)
- {
- _buffer = buffer;
- _codeUnitLengthOfCurrentScalar = 0;
- CurrentCodepoint = Codepoint.ReplacementCodepoint;
- CurrentType = GraphemeBreakClass.Other;
- CurrentCodeUnitOffset = 0;
- }
-
- public int CurrentCodeUnitOffset { get; private set; }
-
- ///
- /// Will be if invalid data or EOF reached.
- /// Caller shouldn't need to special-case this since the normal rules will halt on this condition.
- ///
- public GraphemeBreakClass CurrentType { get; private set; }
-
- ///
- /// Get the currently processed .
- ///
- public Codepoint CurrentCodepoint { get; private set; }
-
- public void MoveNext()
- {
- // For ill-formed subsequences (like unpaired UTF-16 surrogate code points), we rely on
- // the decoder's default behavior of interpreting these ill-formed subsequences as
- // equivalent to U+FFFD REPLACEMENT CHARACTER. This code point has a boundary property
- // of Other (XX), which matches the modifications made to UAX#29, Rev. 35.
- // See: https://www.unicode.org/reports/tr29/tr29-35.html#Modifications
- // This change is also reflected in the UCD files. For example, Unicode 11.0's UCD file
- // https://www.unicode.org/Public/11.0.0/ucd/auxiliary/GraphemeBreakProperty.txt
- // has the line "D800..DFFF ; Control # Cs [2048] ..",
- // but starting with Unicode 12.0 that line has been removed.
- //
- // If a later version of the Unicode Standard further modifies this guidance we should reflect
- // that here.
-
- if (CurrentCodeUnitOffset == _buffer.Length)
- {
- CurrentCodepoint = Codepoint.ReplacementCodepoint;
- }
- else
- {
- CurrentCodeUnitOffset += _codeUnitLengthOfCurrentScalar;
-
- if (CurrentCodeUnitOffset < _buffer.Length)
- {
- CurrentCodepoint = Codepoint.ReadAt(_buffer, CurrentCodeUnitOffset,
- out _codeUnitLengthOfCurrentScalar);
- }
- else
- {
- CurrentCodepoint = Codepoint.ReplacementCodepoint;
- }
- }
-
- CurrentType = CurrentCodepoint.GraphemeBreakClass;
- }
+ // For ill-formed subsequences (like unpaired UTF-16 surrogate code points), we rely on
+ // the decoder's default behavior of interpreting these ill-formed subsequences as
+ // equivalent to U+FFFD REPLACEMENT CHARACTER. This code point has a boundary property
+ // of Other (XX), which matches the modifications made to UAX#29, Rev. 35.
+ // See: https://www.unicode.org/reports/tr29/tr29-35.html#Modifications
+ // This change is also reflected in the UCD files. For example, Unicode 11.0's UCD file
+ // https://www.unicode.org/Public/11.0.0/ucd/auxiliary/GraphemeBreakProperty.txt
+ // has the line "D800..DFFF ; Control # Cs [2048] ..",
+ // but starting with Unicode 12.0 that line has been removed.
+ //
+ // If a later version of the Unicode Standard further modifies this guidance we should reflect
+ // that here.
+
+ _currentCodeUnitOffset += _codeUnitLengthOfCurrentCodepoint;
+
+ _currentCodepoint = Codepoint.ReadAt(_text, _currentCodeUnitOffset,
+ out _codeUnitLengthOfCurrentCodepoint);
+
+ _currentType = _currentCodepoint.GraphemeBreakClass;
}
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
index 877ab76ce5..5e12b7458e 100644
--- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
@@ -3,6 +3,7 @@
// Ported from: https://github.com/SixLabors/Fonts/
using System;
+using System.Runtime.CompilerServices;
namespace Avalonia.Media.TextFormatting.Unicode
{
@@ -46,10 +47,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
_lb30 = false;
_lb30a = 0;
}
-
- public LineBreak Current { get; private set; }
-
- public bool MoveNext()
+
+ public bool MoveNext(out LineBreak lineBreak)
{
// Get the first char if we're at the beginning of the string.
if (_first)
@@ -75,7 +74,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
case LineBreakClass.CarriageReturn when _nextClass != LineBreakClass.LineFeed:
{
_currentClass = MapFirst(_nextClass);
- Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, true);
+ lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, true);
return true;
}
}
@@ -87,7 +86,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
if (shouldBreak)
{
- Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition);
+ lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition);
return true;
}
}
@@ -108,23 +107,23 @@ namespace Avalonia.Media.TextFormatting.Unicode
break;
}
- Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, required);
+ lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, required);
return true;
}
}
- Current = default;
-
+ lineBreak = default;
return false;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static LineBreakClass MapClass(Codepoint cp)
{
if (cp.Value == 327685)
{
return LineBreakClass.Alphabetic;
}
-
+
// LB 1
// ==========================================
// Resolved Original General_Category
@@ -133,26 +132,38 @@ namespace Avalonia.Media.TextFormatting.Unicode
// CM SA Only Mn or Mc
// AL SA Any except Mn and Mc
// NS CJ Any
- switch (cp.LineBreakClass)
- {
- case LineBreakClass.Ambiguous:
- case LineBreakClass.Surrogate:
- case LineBreakClass.Unknown:
- return LineBreakClass.Alphabetic;
-
- case LineBreakClass.ComplexContext:
- return cp.GeneralCategory == GeneralCategory.NonspacingMark || cp.GeneralCategory == GeneralCategory.SpacingMark
- ? LineBreakClass.CombiningMark
- : LineBreakClass.Alphabetic;
+ var cls = cp.LineBreakClass;
- case LineBreakClass.ConditionalJapaneseStarter:
- return LineBreakClass.Nonstarter;
+ const ulong specialMask =
+ (1UL << (int)LineBreakClass.Ambiguous) |
+ (1UL << (int)LineBreakClass.Surrogate) |
+ (1UL << (int)LineBreakClass.Unknown) |
+ (1UL << (int)LineBreakClass.ComplexContext) |
+ (1UL << (int)LineBreakClass.ConditionalJapaneseStarter);
- default:
- return cp.LineBreakClass;
+ if (((1UL << (int)cls) & specialMask) != 0UL)
+ {
+ switch (cls)
+ {
+ case LineBreakClass.Ambiguous:
+ case LineBreakClass.Surrogate:
+ case LineBreakClass.Unknown:
+ return LineBreakClass.Alphabetic;
+
+ case LineBreakClass.ComplexContext:
+ return cp.GeneralCategory is GeneralCategory.NonspacingMark or GeneralCategory.SpacingMark
+ ? LineBreakClass.CombiningMark
+ : LineBreakClass.Alphabetic;
+
+ case LineBreakClass.ConditionalJapaneseStarter:
+ return LineBreakClass.Nonstarter;
+ }
}
+
+ return cls;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static LineBreakClass MapFirst(LineBreakClass c)
{
switch (c)
@@ -169,10 +180,80 @@ namespace Avalonia.Media.TextFormatting.Unicode
}
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAlphaNumeric(LineBreakClass cls)
- => cls == LineBreakClass.Alphabetic
- || cls == LineBreakClass.HebrewLetter
- || cls == LineBreakClass.Numeric;
+ {
+ const ulong mask =
+ (1UL << (int)LineBreakClass.Alphabetic) |
+ (1UL << (int)LineBreakClass.HebrewLetter) |
+ (1UL << (int)LineBreakClass.Numeric);
+
+ return ((1UL << (int)cls) & mask) != 0UL;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsPrefixPostfixNumericOrSpace(LineBreakClass cls)
+ {
+ const ulong mask =
+ (1UL << (int)LineBreakClass.PostfixNumeric) |
+ (1UL << (int)LineBreakClass.PrefixNumeric) |
+ (1UL << (int)LineBreakClass.Space);
+
+ return ((1UL << (int)cls) & mask) != 0UL;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsPrefixPostfixNumeric(LineBreakClass cls)
+ {
+ const ulong mask =
+ (1UL << (int)LineBreakClass.PostfixNumeric) |
+ (1UL << (int)LineBreakClass.PrefixNumeric);
+
+ return ((1UL << (int)cls) & mask) != 0UL;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsClosePunctuationOrParenthesis(LineBreakClass cls)
+ {
+ const ulong mask =
+ (1UL << (int)LineBreakClass.ClosePunctuation) |
+ (1UL << (int)LineBreakClass.CloseParenthesis);
+
+ return ((1UL << (int)cls) & mask) != 0UL;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsClosePunctuationOrInfixNumericOrBreakSymbols(LineBreakClass cls)
+ {
+ const ulong mask =
+ (1UL << (int)LineBreakClass.ClosePunctuation) |
+ (1UL << (int)LineBreakClass.InfixNumeric) |
+ (1UL << (int)LineBreakClass.BreakSymbols);
+
+ return ((1UL << (int)cls) & mask) != 0UL;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsSpaceOrWordJoinerOrAlphabetic(LineBreakClass cls)
+ {
+ const ulong mask =
+ (1UL << (int)LineBreakClass.Space) |
+ (1UL << (int)LineBreakClass.WordJoiner) |
+ (1UL << (int)LineBreakClass.Alphabetic);
+
+ return ((1UL << (int)cls) & mask) != 0UL;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsMandatoryBreakOrLineFeedOrCarriageReturn(LineBreakClass cls)
+ {
+ const ulong mask =
+ (1UL << (int)LineBreakClass.MandatoryBreak) |
+ (1UL << (int)LineBreakClass.LineFeed) |
+ (1UL << (int)LineBreakClass.CarriageReturn);
+
+ return ((1UL << (int)cls) & mask) != 0UL;
+ }
private LineBreakClass PeekNextCharClass()
{
@@ -198,83 +279,77 @@ namespace Avalonia.Media.TextFormatting.Unicode
// Track combining mark exceptions. LB22
if (cls == LineBreakClass.CombiningMark)
{
- switch (_currentClass)
+ const ulong lb22ExMask =
+ (1UL << (int)LineBreakClass.MandatoryBreak) |
+ (1UL << (int)LineBreakClass.ContingentBreak) |
+ (1UL << (int)LineBreakClass.Exclamation) |
+ (1UL << (int)LineBreakClass.LineFeed) |
+ (1UL << (int)LineBreakClass.NextLine) |
+ (1UL << (int)LineBreakClass.Space) |
+ (1UL << (int)LineBreakClass.ZWSpace) |
+ (1UL << (int)LineBreakClass.CarriageReturn);
+
+ if (((1UL << (int)_currentClass) & lb22ExMask) != 0UL)
{
- case LineBreakClass.MandatoryBreak:
- case LineBreakClass.ContingentBreak:
- case LineBreakClass.Exclamation:
- case LineBreakClass.LineFeed:
- case LineBreakClass.NextLine:
- case LineBreakClass.Space:
- case LineBreakClass.ZWSpace:
- case LineBreakClass.CarriageReturn:
- _lb22ex = true;
- break;
+ _lb22ex = true;
}
- }
- // Track combining mark exceptions. LB31
- if (_first && cls == LineBreakClass.CombiningMark)
- {
- _lb31 = true;
+ const ulong lb31Mask =
+ (1UL << (int)LineBreakClass.MandatoryBreak) |
+ (1UL << (int)LineBreakClass.ContingentBreak) |
+ (1UL << (int)LineBreakClass.Exclamation) |
+ (1UL << (int)LineBreakClass.LineFeed) |
+ (1UL << (int)LineBreakClass.NextLine) |
+ (1UL << (int)LineBreakClass.Space) |
+ (1UL << (int)LineBreakClass.ZWSpace) |
+ (1UL << (int)LineBreakClass.CarriageReturn) |
+ (1UL << (int)LineBreakClass.ZWJ);
+
+ // Track combining mark exceptions. LB31
+ if (_first || ((1UL << (int)_currentClass) & lb31Mask) != 0UL)
+ {
+ _lb31 = true;
+ }
}
- if (cls == LineBreakClass.CombiningMark)
+ if (_first)
{
- switch (_currentClass)
+ // Rule LB24
+ if (IsClosePunctuationOrParenthesis(cls))
{
- case LineBreakClass.MandatoryBreak:
- case LineBreakClass.ContingentBreak:
- case LineBreakClass.Exclamation:
- case LineBreakClass.LineFeed:
- case LineBreakClass.NextLine:
- case LineBreakClass.Space:
- case LineBreakClass.ZWSpace:
- case LineBreakClass.CarriageReturn:
- case LineBreakClass.ZWJ:
- _lb31 = true;
- break;
+ _lb24ex = true;
}
- }
- if (_first
- && (cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space))
- {
- _lb31 = true;
+ // Rule LB25
+ if (IsClosePunctuationOrInfixNumericOrBreakSymbols(cls))
+ {
+ _lb25ex = true;
+ }
+
+ if (IsPrefixPostfixNumericOrSpace(cls))
+ {
+ _lb31 = true;
+ }
}
- if (_currentClass == LineBreakClass.Alphabetic &&
- (cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space))
+ if (_currentClass == LineBreakClass.Alphabetic && IsPrefixPostfixNumericOrSpace(cls))
{
_lb31 = true;
}
// Reset LB31 if next is U+0028 (Left Opening Parenthesis)
if (_lb31
- && _currentClass != LineBreakClass.PostfixNumeric
- && _currentClass != LineBreakClass.PrefixNumeric
- && cls == LineBreakClass.OpenPunctuation && cp.Value == 0x0028)
+ && !IsPrefixPostfixNumeric(_currentClass)
+ && cls == LineBreakClass.OpenPunctuation
+ && cp.Value == 0x0028)
{
_lb31 = false;
}
- // Rule LB24
- if (_first && (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.CloseParenthesis))
- {
- _lb24ex = true;
- }
-
- // Rule LB25
- if (_first
- && (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.InfixNumeric || cls == LineBreakClass.BreakSymbols))
- {
- _lb25ex = true;
- }
-
- if (cls == LineBreakClass.Space || cls == LineBreakClass.WordJoiner || cls == LineBreakClass.Alphabetic)
+ if (IsSpaceOrWordJoinerOrAlphabetic(cls))
{
var next = PeekNextCharClass();
- if (next == LineBreakClass.ClosePunctuation || next == LineBreakClass.InfixNumeric || next == LineBreakClass.BreakSymbols)
+ if (IsClosePunctuationOrInfixNumericOrBreakSymbols(next))
{
_lb25ex = true;
}
@@ -295,6 +370,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
return cls;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool? GetSimpleBreak()
{
// handle classes not handled by the pair table
@@ -317,6 +393,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
return null;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] // quite long but only one usage
private bool GetPairTableBreak(LineBreakClass lastClass)
{
// If not handled already, use the pair table
@@ -477,8 +554,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
var cls = cp.LineBreakClass;
- if (cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed ||
- cls == LineBreakClass.CarriageReturn)
+ if (IsMandatoryBreakOrLineFeedOrCarriageReturn(cls))
{
from -= count;
}
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 9d07fb024a..78caf350b7 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -963,10 +963,8 @@ namespace Avalonia.Controls
var graphemeEnumerator = new GraphemeEnumerator(input.AsSpan());
- while (graphemeEnumerator.MoveNext())
+ while (graphemeEnumerator.MoveNext(out var grapheme))
{
- var grapheme = graphemeEnumerator.Current;
-
if (grapheme.FirstCodepoint.IsBreakChar)
{
if (lineCount + 1 > MaxLines)
@@ -979,7 +977,7 @@ namespace Avalonia.Controls
}
}
- length += grapheme.Text.Length;
+ length += grapheme.Length;
}
if (length < input.Length)
diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
index ffe8352865..4c3cfe2ef4 100644
--- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
+++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
@@ -23,6 +23,7 @@
+
diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
index def2482af3..e1a6b93692 100644
--- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs
+++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
@@ -52,6 +52,8 @@ namespace Avalonia.Skia
var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
+ var targetInfos = shapedBuffer.GlyphInfos;
+
var glyphInfos = buffer.GetGlyphInfoSpan();
var glyphPositions = buffer.GetGlyphPositionSpan();
@@ -77,9 +79,7 @@ namespace Avalonia.Skia
4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
}
- var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
-
- shapedBuffer[i] = targetInfo;
+ targetInfos[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
}
return shapedBuffer;
diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
index ac441108e3..ff0fff6b14 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
@@ -52,6 +52,8 @@ namespace Avalonia.Direct2D1.Media
var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
+ var targetInfos = shapedBuffer.GlyphInfos;
+
var glyphInfos = buffer.GetGlyphInfoSpan();
var glyphPositions = buffer.GetGlyphPositionSpan();
@@ -77,9 +79,7 @@ namespace Avalonia.Direct2D1.Media
4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
}
- var targetInfo = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
-
- shapedBuffer[i] = targetInfo;
+ targetInfos[i] = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
}
return shapedBuffer;
diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs
index a022039000..0e49669a04 100644
--- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs
@@ -40,9 +40,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
var enumerator = new GraphemeEnumerator(text);
- enumerator.MoveNext();
+ enumerator.MoveNext(out var g);
- var actual = enumerator.Current.Text;
+ var actual = text.AsSpan(g.Offset, g.Length);
bool pass = actual.Length == grapheme.Length;
@@ -86,9 +86,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
var count = 0;
- while (enumerator.MoveNext())
+ while (enumerator.MoveNext(out var grapheme))
{
- Assert.Equal(1, enumerator.Current.Text.Length);
+ Assert.Equal(1, grapheme.Length);
count++;
}
diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs
index d198fe81a6..3db9a32b65 100644
--- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs
@@ -24,32 +24,33 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
public void BasicLatinTest()
{
var lineBreaker = new LineBreakEnumerator("Hello World\r\nThis is a test.");
+ LineBreak lineBreak;
- Assert.True(lineBreaker.MoveNext());
- Assert.Equal(6, lineBreaker.Current.PositionWrap);
- Assert.False(lineBreaker.Current.Required);
+ Assert.True(lineBreaker.MoveNext(out lineBreak));
+ Assert.Equal(6, lineBreak.PositionWrap);
+ Assert.False(lineBreak.Required);
- Assert.True(lineBreaker.MoveNext());
- Assert.Equal(13, lineBreaker.Current.PositionWrap);
- Assert.True(lineBreaker.Current.Required);
+ Assert.True(lineBreaker.MoveNext(out lineBreak));
+ Assert.Equal(13, lineBreak.PositionWrap);
+ Assert.True(lineBreak.Required);
- Assert.True(lineBreaker.MoveNext());
- Assert.Equal(18, lineBreaker.Current.PositionWrap);
- Assert.False(lineBreaker.Current.Required);
+ Assert.True(lineBreaker.MoveNext(out lineBreak));
+ Assert.Equal(18, lineBreak.PositionWrap);
+ Assert.False(lineBreak.Required);
- Assert.True(lineBreaker.MoveNext());
- Assert.Equal(21, lineBreaker.Current.PositionWrap);
- Assert.False(lineBreaker.Current.Required);
+ Assert.True(lineBreaker.MoveNext(out lineBreak));
+ Assert.Equal(21, lineBreak.PositionWrap);
+ Assert.False(lineBreak.Required);
- Assert.True(lineBreaker.MoveNext());
- Assert.Equal(23, lineBreaker.Current.PositionWrap);
- Assert.False(lineBreaker.Current.Required);
+ Assert.True(lineBreaker.MoveNext(out lineBreak));
+ Assert.Equal(23, lineBreak.PositionWrap);
+ Assert.False(lineBreak.Required);
- Assert.True(lineBreaker.MoveNext());
- Assert.Equal(28, lineBreaker.Current.PositionWrap);
- Assert.False(lineBreaker.Current.Required);
+ Assert.True(lineBreaker.MoveNext(out lineBreak));
+ Assert.Equal(28, lineBreak.PositionWrap);
+ Assert.False(lineBreak.Required);
- Assert.False(lineBreaker.MoveNext());
+ Assert.False(lineBreaker.MoveNext(out lineBreak));
}
@@ -72,9 +73,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
{
var breaks = new List();
- while (lineBreaker.MoveNext())
+ while (lineBreaker.MoveNext(out var lineBreak))
{
- breaks.Add(lineBreaker.Current);
+ breaks.Add(lineBreak);
}
return breaks;
@@ -104,9 +105,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
var foundBreaks = new List();
- while (lineBreaker.MoveNext())
+ while (lineBreaker.MoveNext(out var lineBreak))
{
- foundBreaks.Add(lineBreaker.Current.PositionWrap);
+ foundBreaks.Add(lineBreak.PositionWrap);
}
// Check the same
diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
index 941d377a17..0ddee2ad7a 100644
--- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
+++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
@@ -10,6 +10,7 @@
+
diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs
index c96edbef5c..4dad8442de 100644
--- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs
+++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs
@@ -3,6 +3,7 @@ using System.Linq;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
+using Avalonia.Skia;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
@@ -13,24 +14,35 @@ namespace Avalonia.Benchmarks.Text;
[MaxWarmupCount(15)]
public class HugeTextLayout : IDisposable
{
+ private static readonly Random s_rand = new();
+ private static readonly bool s_useSkia = true;
+
private readonly IDisposable _app;
- private string[] _manySmallStrings;
- private static Random _rand = new Random();
-
+ private readonly string[] _manySmallStrings;
+
private static string RandomString(int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789&?%$@";
- return new string(Enumerable.Repeat(chars, length).Select(s => s[_rand.Next(s.Length)]).ToArray());
+ return new string(Enumerable.Repeat(chars, length).Select(s => s[s_rand.Next(s.Length)]).ToArray());
}
public HugeTextLayout()
{
- _manySmallStrings = Enumerable.Range(0, 1000).Select(x => RandomString(_rand.Next(2, 15))).ToArray();
- _app = UnitTestApplication.Start(
- TestServices.StyledWindow.With(
- renderInterface: new NullRenderingPlatform(),
- threadingInterface: new NullThreadingPlatform(),
- standardCursorFactory: new NullCursorFactory()));
+ _manySmallStrings = Enumerable.Range(0, 1000).Select(_ => RandomString(s_rand.Next(2, 15))).ToArray();
+
+ var testServices = TestServices.StyledWindow.With(
+ renderInterface: new NullRenderingPlatform(),
+ threadingInterface: new NullThreadingPlatform(),
+ standardCursorFactory: new NullCursorFactory());
+
+ if (s_useSkia)
+ {
+ testServices = testServices.With(
+ textShaperImpl: new TextShaperImpl(),
+ fontManagerImpl: new FontManagerImpl());
+ }
+
+ _app = UnitTestApplication.Start(testServices);
}
private const string Text = @"Though, the objectives of the development of the prominent landmarks can be neglected in most cases, it should be realized that after the completion of the strategic decision gives rise to The Expertise of Regular Program (Carlton Cartwright in The Book of the Key Factor)
@@ -77,7 +89,17 @@ In respect that the structure of the sufficient amount poses problems and challe
public TextLayout BuildEmojisTextLayout() => MakeLayout(Emojis);
[Benchmark]
- public TextLayout[] BuildManySmallTexts() => _manySmallStrings.Select(MakeLayout).ToArray();
+ public TextLayout[] BuildManySmallTexts()
+ {
+ var results = new TextLayout[_manySmallStrings.Length];
+
+ for (var i = 0; i < _manySmallStrings.Length; i++)
+ {
+ results[i] = MakeLayout(_manySmallStrings[i]);
+ }
+
+ return results;
+ }
[Benchmark]
public void VirtualizeTextBlocks()
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
index 6b9fb579b1..7822d6624b 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
@@ -283,9 +283,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var expected = new List();
- while (lineBreaker.MoveNext())
+ while (lineBreaker.MoveNext(out var lineBreak))
{
- expected.Add(lineBreaker.Current.PositionWrap - 1);
+ expected.Add(lineBreak.PositionWrap - 1);
}
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" +
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
index a24a0fcf70..2b63f24cf6 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
@@ -151,9 +151,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
while (true)
{
- while (inner.MoveNext())
+ Grapheme grapheme;
+ while (inner.MoveNext(out grapheme))
{
- j += inner.Current.Text.Length;
+ j += grapheme.Length;
if (j + i > text.Length)
{
@@ -184,14 +185,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
- if (!outer.MoveNext())
+ if (!outer.MoveNext(out grapheme))
{
break;
}
inner = new GraphemeEnumerator(text);
- i += outer.Current.Text.Length;
+ i += grapheme.Length;
}
}
@@ -979,13 +980,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var graphemeEnumerator = new GraphemeEnumerator(text);
- while (graphemeEnumerator.MoveNext())
+ while (graphemeEnumerator.MoveNext(out var grapheme))
{
- var grapheme = graphemeEnumerator.Current;
+ var textStyleOverrides = new[] { new ValueSpan(i, grapheme.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) };
- var textStyleOverrides = new[] { new ValueSpan(i, grapheme.Text.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) };
-
- i += grapheme.Text.Length;
+ i += grapheme.Length;
var layout = new TextLayout(
text,
diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
index baf5ffb07c..0448ecd41f 100644
--- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
+++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
@@ -52,6 +52,8 @@ namespace Avalonia.UnitTests
var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
+ var targetInfos = shapedBuffer.GlyphInfos;
+
var glyphInfos = buffer.GetGlyphInfoSpan();
var glyphPositions = buffer.GetGlyphPositionSpan();
@@ -77,9 +79,7 @@ namespace Avalonia.UnitTests
4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
}
- var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
-
- shapedBuffer[i] = targetInfo;
+ targetInfos[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
}
return shapedBuffer;
diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs
index 45e7b47f62..477f34565f 100644
--- a/tests/Avalonia.UnitTests/MockGlyphRun.cs
+++ b/tests/Avalonia.UnitTests/MockGlyphRun.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
-using System.Linq;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
@@ -9,7 +8,14 @@ namespace Avalonia.UnitTests
{
public MockGlyphRun(IReadOnlyList glyphInfos)
{
- Size = new Size(glyphInfos.Sum(x=> x.GlyphAdvance), 10);
+ var width = 0.0;
+
+ for (var i = 0; i < glyphInfos.Count; ++i)
+ {
+ width += glyphInfos[i].GlyphAdvance;
+ }
+
+ Size = new Size(width, 10);
}
public Size Size { get; }
diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
index b5f4777192..b810caabd9 100644
--- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
+++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
@@ -13,6 +13,7 @@ namespace Avalonia.UnitTests
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
+ var targetInfos = shapedBuffer.GlyphInfos;
var textSpan = text.Span;
var textStartIndex = TextTestHelper.GetStartCharIndex(text);
@@ -26,7 +27,7 @@ namespace Avalonia.UnitTests
for (var j = 0; j < count; ++j)
{
- shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10);
+ targetInfos[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10);
}
i += count;