using System; using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { public sealed class ShapedBuffer : IReadOnlyList, IDisposable { private GlyphInfo[]? _rentedBuffer; private ArraySlice _glyphInfos; public ShapedBuffer(ReadOnlyMemory text, int bufferLength, GlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { Text = text; _rentedBuffer = ArrayPool.Shared.Rent(bufferLength); _glyphInfos = new ArraySlice(_rentedBuffer, 0, bufferLength); GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; BidiLevel = bidiLevel; } internal ShapedBuffer(ReadOnlyMemory text, ArraySlice glyphInfos, GlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { Text = text; _glyphInfos = glyphInfos; GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; BidiLevel = bidiLevel; } /// /// The buffer's length. /// public int Length => _glyphInfos.Length; /// /// The buffer's glyph typeface. /// public GlyphTypeface GlyphTypeface { get; } /// /// The buffers font rendering em size. /// public double FontRenderingEmSize { get; } /// /// The buffer's bidi level. /// public sbyte BidiLevel { get; private set; } /// /// The buffer's reading direction. /// public bool IsLeftToRight => (BidiLevel & 1) == 0; /// /// The text that is represended by this buffer. /// public ReadOnlyMemory Text { get; } /// /// Reverses the buffer. /// public void Reverse() { _glyphInfos.Span.Reverse(); } public void Dispose() { if (_rentedBuffer is not null) { ArrayPool.Shared.Return(_rentedBuffer); _rentedBuffer = null; _glyphInfos = ArraySlice.Empty; // ensure we don't misuse the returned array } } public GlyphInfo this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _glyphInfos[index]; [MethodImpl(MethodImplOptions.AggressiveInlining)] set => _glyphInfos[index] = value; } public IEnumerator GetEnumerator() => _glyphInfos.GetEnumerator(); internal void ResetBidiLevel(sbyte paragraphEmbeddingLevel) => BidiLevel = paragraphEmbeddingLevel; int IReadOnlyCollection.Count => _glyphInfos.Length; IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// /// Splits the at specified length. /// /// The text length. /// The split result. public SplitResult Split(int textLength) { // make sure we do not overshoot textLength = Math.Min(Text.Length, textLength); if (textLength <= 0) { var emptyBuffer = new ShapedBuffer( Text.Slice(0, 0), _glyphInfos.Slice(_glyphInfos.Start, 0), GlyphTypeface, FontRenderingEmSize, BidiLevel); return new SplitResult(emptyBuffer, this); } // nothing to split if (textLength == Text.Length) { return new SplitResult(this, null); } var sliceStart = _glyphInfos.Start; var glyphInfos = _glyphInfos.Span; var glyphInfosLength = _glyphInfos.Length; // 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; // binary‐search for a dummy with cluster == targetCluster var searchValue = new GlyphInfo(0, targetCluster, 0, default); var foundIndex = glyphInfos.BinarySearch(searchValue, GlyphInfo.ClusterAscendingComparer); int splitGlyphIndex; // how many glyph‐slots go into "leading" int splitCharCount; // how many chars go into "leading" Text if (foundIndex >= 0) { // found a glyph info whose cluster == targetCluster // back up to the start of the cluster var i = foundIndex; while (i > 0 && glyphInfos[i - 1].GlyphCluster == targetCluster) { i--; } splitGlyphIndex = i; splitCharCount = targetCluster - baseCluster; } else { // no exact match need to invert so ~foundIndex is the insertion point // the first cluster > targetCluster var invertedIndex = ~foundIndex; 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 firstGlyphs = _glyphInfos.Slice(sliceStart, splitGlyphIndex); var secondGlyphs = _glyphInfos.Slice(sliceStart + splitGlyphIndex, glyphInfosLength - splitGlyphIndex); var firstText = Text.Slice(0, splitCharCount); var secondText = Text.Slice(splitCharCount); var leading = new ShapedBuffer( firstText, firstGlyphs, 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(leading, null); } var trailing = new ShapedBuffer( secondText, secondGlyphs, GlyphTypeface, FontRenderingEmSize, BidiLevel); return new SplitResult(leading, trailing); } } }