|
|
|
@ -2,6 +2,7 @@ |
|
|
|
using System.Buffers; |
|
|
|
using System.Collections; |
|
|
|
using System.Collections.Generic; |
|
|
|
using System.Linq; |
|
|
|
using System.Runtime.CompilerServices; |
|
|
|
using Avalonia.Utilities; |
|
|
|
|
|
|
|
@ -89,90 +90,110 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
public IEnumerator<GlyphInfo> GetEnumerator() => _glyphInfos.GetEnumerator(); |
|
|
|
|
|
|
|
internal void ResetBidiLevel(sbyte paragraphEmbeddingLevel) => BidiLevel = paragraphEmbeddingLevel; |
|
|
|
|
|
|
|
int IReadOnlyCollection<GlyphInfo>.Count => _glyphInfos.Length; |
|
|
|
|
|
|
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Finds a glyph index for given character index.
|
|
|
|
/// Splits the <see cref="TextRun"/> at specified length.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="characterIndex">The character index.</param>
|
|
|
|
/// <returns>
|
|
|
|
/// The glyph index.
|
|
|
|
/// </returns>
|
|
|
|
private int FindGlyphIndex(int characterIndex) |
|
|
|
/// <param name="textLength">The text length.</param>
|
|
|
|
/// <returns>The split result.</returns>
|
|
|
|
public SplitResult<ShapedBuffer> Split(int textLength) |
|
|
|
{ |
|
|
|
if (characterIndex < _glyphInfos[0].GlyphCluster) |
|
|
|
// make sure we do not overshoot
|
|
|
|
textLength = Math.Min(Text.Length, textLength); |
|
|
|
|
|
|
|
if (textLength <= 0) |
|
|
|
{ |
|
|
|
return 0; |
|
|
|
var emptyBuffer = new ShapedBuffer( |
|
|
|
Text.Slice(0, 0), _glyphInfos.Slice(_glyphInfos.Start, 0), |
|
|
|
GlyphTypeface, FontRenderingEmSize, BidiLevel); |
|
|
|
|
|
|
|
return new SplitResult<ShapedBuffer>(emptyBuffer, this); |
|
|
|
} |
|
|
|
|
|
|
|
if (characterIndex > _glyphInfos[_glyphInfos.Length - 1].GlyphCluster) |
|
|
|
// nothing to split
|
|
|
|
if (textLength == Text.Length) |
|
|
|
{ |
|
|
|
return _glyphInfos.Length - 1; |
|
|
|
return new SplitResult<ShapedBuffer>(this, null); |
|
|
|
} |
|
|
|
|
|
|
|
var comparer = GlyphInfo.ClusterAscendingComparer; |
|
|
|
|
|
|
|
var sliceStart = _glyphInfos.Start; |
|
|
|
var glyphInfos = _glyphInfos.Span; |
|
|
|
var glyphInfosLength = _glyphInfos.Length; |
|
|
|
|
|
|
|
var searchValue = new GlyphInfo(default, characterIndex, default); |
|
|
|
// the first glyph’s cluster is our “zero” for this sub‐buffer.
|
|
|
|
// we want an absolute target cluster = baseCluster + textLength
|
|
|
|
var baseCluster = glyphInfos[0].GlyphCluster; |
|
|
|
var targetCluster = baseCluster + textLength; |
|
|
|
|
|
|
|
var start = glyphInfos.BinarySearch(searchValue, comparer); |
|
|
|
// binary‐search for a dummy with cluster == targetCluster
|
|
|
|
var searchValue = new GlyphInfo(0, targetCluster, 0, default); |
|
|
|
var foundIndex = glyphInfos.BinarySearch(searchValue, GlyphInfo.ClusterAscendingComparer); |
|
|
|
|
|
|
|
if (start < 0) |
|
|
|
{ |
|
|
|
while (characterIndex > 0 && start < 0) |
|
|
|
{ |
|
|
|
characterIndex--; |
|
|
|
int splitGlyphIndex; // how many glyph‐slots go into "leading"
|
|
|
|
int splitCharCount; // how many chars go into "leading" Text
|
|
|
|
|
|
|
|
searchValue = new GlyphInfo(default, characterIndex, default); |
|
|
|
|
|
|
|
start = glyphInfos.BinarySearch(searchValue, comparer); |
|
|
|
} |
|
|
|
if (foundIndex >= 0) |
|
|
|
{ |
|
|
|
// found a glyph info whose cluster == targetCluster
|
|
|
|
// back up to the start of the cluster
|
|
|
|
var i = foundIndex; |
|
|
|
|
|
|
|
if (start < 0) |
|
|
|
while (i > 0 && glyphInfos[i - 1].GlyphCluster == targetCluster) |
|
|
|
{ |
|
|
|
return -1; |
|
|
|
i--; |
|
|
|
} |
|
|
|
|
|
|
|
splitGlyphIndex = i; |
|
|
|
splitCharCount = targetCluster - baseCluster; |
|
|
|
} |
|
|
|
|
|
|
|
while (start > 0 && glyphInfos[start - 1].GlyphCluster == glyphInfos[start].GlyphCluster) |
|
|
|
else |
|
|
|
{ |
|
|
|
start--; |
|
|
|
} |
|
|
|
|
|
|
|
return start; |
|
|
|
} |
|
|
|
// no exact match need to invert so ~foundIndex is the insertion point
|
|
|
|
// the first cluster > targetCluster
|
|
|
|
var invertedIndex = ~foundIndex; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Splits the <see cref="TextRun"/> at specified length.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="length">The length.</param>
|
|
|
|
/// <returns>The split result.</returns>
|
|
|
|
internal SplitResult<ShapedBuffer> Split(int length) |
|
|
|
{ |
|
|
|
if (Text.Length == length) |
|
|
|
{ |
|
|
|
return new SplitResult<ShapedBuffer>(this, null); |
|
|
|
if (invertedIndex >= glyphInfosLength) |
|
|
|
{ |
|
|
|
// happens only if targetCluster ≥ lastCluster
|
|
|
|
// put everything into leading
|
|
|
|
splitGlyphIndex = glyphInfosLength; |
|
|
|
splitCharCount = Text.Length; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
// snap to the start of that next cluster
|
|
|
|
splitGlyphIndex = invertedIndex; |
|
|
|
var nextCluster = glyphInfos[invertedIndex].GlyphCluster; |
|
|
|
splitCharCount = nextCluster - baseCluster; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
var firstCluster = _glyphInfos[0].GlyphCluster; |
|
|
|
var lastCluster = _glyphInfos[_glyphInfos.Length - 1].GlyphCluster; |
|
|
|
var firstGlyphs = _glyphInfos.Slice(sliceStart, splitGlyphIndex); |
|
|
|
var secondGlyphs = _glyphInfos.Slice(sliceStart + splitGlyphIndex, glyphInfosLength - splitGlyphIndex); |
|
|
|
|
|
|
|
var start = firstCluster < lastCluster ? firstCluster : lastCluster; |
|
|
|
var firstText = Text.Slice(0, splitCharCount); |
|
|
|
var secondText = Text.Slice(splitCharCount); |
|
|
|
|
|
|
|
var glyphCount = FindGlyphIndex(start + length); |
|
|
|
var leading = new ShapedBuffer( |
|
|
|
firstText, firstGlyphs, |
|
|
|
GlyphTypeface, FontRenderingEmSize, BidiLevel); |
|
|
|
|
|
|
|
var first = new ShapedBuffer(Text.Slice(0, length), |
|
|
|
_glyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); |
|
|
|
// this happens if we try to find a position inside a cluster and we moved to the end
|
|
|
|
if(secondText.Length == 0) |
|
|
|
{ |
|
|
|
return new SplitResult<ShapedBuffer>(leading, null); |
|
|
|
} |
|
|
|
|
|
|
|
var second = new ShapedBuffer(Text.Slice(length), |
|
|
|
_glyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); |
|
|
|
var trailing = new ShapedBuffer( |
|
|
|
secondText, secondGlyphs, |
|
|
|
GlyphTypeface, FontRenderingEmSize, BidiLevel); |
|
|
|
|
|
|
|
return new SplitResult<ShapedBuffer>(first, second); |
|
|
|
return new SplitResult<ShapedBuffer>(leading, trailing); |
|
|
|
} |
|
|
|
|
|
|
|
internal void ResetBidiLevel(sbyte paragraphEmbeddingLevel) => BidiLevel = paragraphEmbeddingLevel; |
|
|
|
|
|
|
|
int IReadOnlyCollection<GlyphInfo>.Count => _glyphInfos.Length; |
|
|
|
|
|
|
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|
|
|
} |
|
|
|
} |
|
|
|
|