From b752c3248191f7d71f87afa84f37af5f55173907 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 13 May 2025 07:44:45 +0200 Subject: [PATCH] Multiple TextLine.GetTextRunBounds fixes (#18749) * Adjust TextLineImpl.GetTextRunBounds so it properly handles substitutions Adjust TextLineImpl.GetTextRunBounds so it properly reports out of text range bounds Adjust TextLineImpl.GetTextRunBounds so it properly reports text source run indices * Remove redundant comments * Add requested changes --- .../Media/TextFormatting/TextLineImpl.cs | 298 ++++++++++------- .../Media/TextFormatting/TextLineTests.cs | 316 ++++++++++++++++++ 2 files changed, 496 insertions(+), 118 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index c038df9468..f13fd26f27 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -602,101 +602,40 @@ namespace Avalonia.Media.TextFormatting public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) { - if (_indexedTextRuns is null || _indexedTextRuns.Count == 0) + if(textLength == 0) { - return Array.Empty(); + throw new ArgumentOutOfRangeException(nameof(textLength), textLength, $"{nameof(textLength)} ('0') must be a non-zero value. "); } - var result = new List(); + if (_indexedTextRuns is null || _indexedTextRuns.Count == 0) + { + return []; + } var currentPosition = FirstTextSourceIndex; var remainingLength = textLength; - TextBounds? lastBounds = null; - - static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection) + //We can return early if the requested text range is before the line's text range. + if (firstTextSourceIndex + textLength < FirstTextSourceIndex) { - if (textRun is ShapedTextRun shapedTextRun) - { - return shapedTextRun.ShapedBuffer.IsLeftToRight ? - FlowDirection.LeftToRight : - FlowDirection.RightToLeft; - } + var indexedTextRun = _indexedTextRuns[0]; + var currentDirection = GetDirection(indexedTextRun.TextRun, _resolvedFlowDirection); - return currentDirection; + return [new TextBounds(new Rect(0,0,0, Height), currentDirection, [])]; } - IndexedTextRun FindIndexedRun() + //We can return early if the requested text range is after the line's text range. + if (firstTextSourceIndex >= FirstTextSourceIndex + Length) { - var i = 0; + var indexedTextRun = _indexedTextRuns[_indexedTextRuns.Count - 1]; + var currentDirection = GetDirection(indexedTextRun.TextRun, _resolvedFlowDirection); - IndexedTextRun currentIndexedRun = _indexedTextRuns[i]; - - while (currentIndexedRun.TextSourceCharacterIndex != currentPosition) - { - if (i + 1 == _indexedTextRuns.Count) - { - break; - } - - i++; - - currentIndexedRun = _indexedTextRuns[i]; - } - - return currentIndexedRun; - } - - double GetPreceedingDistance(int firstIndex) - { - var distance = 0.0; - - for (var i = 0; i < firstIndex; i++) - { - var currentRun = _textRuns[i]; - - if (currentRun is DrawableTextRun drawableTextRun) - { - distance += drawableTextRun.Size.Width; - } - } - - return distance; + return [new TextBounds(new Rect(WidthIncludingTrailingWhitespace, 0, 0, Height), currentDirection, [])]; } - bool TryMergeWithLastBounds(TextBounds currentBounds, TextBounds lastBounds) - { - if (currentBounds.FlowDirection != lastBounds.FlowDirection) - { - return false; - } - - if (currentBounds.Rectangle.Left == lastBounds.Rectangle.Right) - { - foreach (var runBounds in currentBounds.TextRunBounds) - { - lastBounds.TextRunBounds.Add(runBounds); - } - - lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle); - - return true; - } - - if (currentBounds.Rectangle.Right == lastBounds.Rectangle.Left) - { - for (int i = 0; i < currentBounds.TextRunBounds.Count; i++) - { - lastBounds.TextRunBounds.Insert(i, currentBounds.TextRunBounds[i]); - } - - lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle); - - return true; - } + var result = new List(); - return false; - } + TextBounds? lastBounds = null; while (remainingLength > 0 && currentPosition < FirstTextSourceIndex + Length) { @@ -733,8 +672,8 @@ namespace Avalonia.Media.TextFormatting directionalWidth = currentDrawable.Size.Width; } + TextBounds currentBounds; int coveredLength; - TextBounds? currentBounds; switch (currentDirection) { @@ -754,12 +693,6 @@ namespace Avalonia.Media.TextFormatting } } - if (coveredLength == 0) - { - //This should never happen - break; - } - if (lastBounds != null && TryMergeWithLastBounds(currentBounds, lastBounds)) { currentBounds = lastBounds; @@ -779,6 +712,90 @@ namespace Avalonia.Media.TextFormatting result.Sort(TextBoundsComparer); return result; + + static FlowDirection GetDirection(TextRun? textRun, FlowDirection currentDirection) + { + if (textRun is ShapedTextRun shapedTextRun) + { + return shapedTextRun.ShapedBuffer.IsLeftToRight ? + FlowDirection.LeftToRight : + FlowDirection.RightToLeft; + } + + return currentDirection; + } + + IndexedTextRun FindIndexedRun() + { + var i = 0; + + var currentIndexedRun = _indexedTextRuns[i]; + + while (currentIndexedRun.TextSourceCharacterIndex != currentPosition) + { + if (i + 1 == _indexedTextRuns.Count) + { + break; + } + + i++; + + currentIndexedRun = _indexedTextRuns[i]; + } + + return currentIndexedRun; + } + + double GetPreceedingDistance(int firstIndex) + { + var distance = 0.0; + + for (var i = 0; i < firstIndex; i++) + { + var currentRun = _textRuns[i]; + + if (currentRun is DrawableTextRun drawableTextRun) + { + distance += drawableTextRun.Size.Width; + } + } + + return distance; + } + + bool TryMergeWithLastBounds(TextBounds currentBounds, TextBounds lastBounds) + { + if (currentBounds.FlowDirection != lastBounds.FlowDirection) + { + return false; + } + + if (currentBounds.Rectangle.Left == lastBounds.Rectangle.Right) + { + foreach (var runBounds in currentBounds.TextRunBounds) + { + lastBounds.TextRunBounds.Add(runBounds); + } + + lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle); + + return true; + } + + if (currentBounds.Rectangle.Right == lastBounds.Rectangle.Left) + { + for (int i = 0; i < currentBounds.TextRunBounds.Count; i++) + { + lastBounds.TextRunBounds.Insert(i, currentBounds.TextRunBounds[i]); + } + + lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle); + + return true; + } + + return false; + } } private CharacterHit GetPreviousCharacterHit(CharacterHit characterHit, bool useGraphemeBoundaries) @@ -885,7 +902,10 @@ namespace Avalonia.Media.TextFormatting { var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); - textRunBounds.Insert(0, runBounds); + if (runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length) + { + textRunBounds.Insert(0, runBounds); + } if (offset > 0) { @@ -904,20 +924,25 @@ namespace Avalonia.Media.TextFormatting } else { - if (currentRun is DrawableTextRun drawableTextRun) + if (currentPosition < FirstTextSourceIndex + Length) { - startX -= drawableTextRun.Size.Width; + if (currentRun is DrawableTextRun drawableTextRun) + { + startX -= drawableTextRun.Size.Width; - textRunBounds.Insert(0, - new TextRunBounds( - new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun)); - } - else - { - //Add potential TextEndOfParagraph - textRunBounds.Add( - new TextRunBounds( - new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun)); + var runBounds = new TextRunBounds( + new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun); + + textRunBounds.Insert(0, runBounds); + } + else + { + //Add potential TextEndOfParagraph + var runBounds = new TextRunBounds( + new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun); + + textRunBounds.Add(runBounds); + } } currentPosition += currentRun.Length; @@ -946,7 +971,7 @@ namespace Avalonia.Media.TextFormatting int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition) { coveredLength = 0; - var textRunBounds = new List(); + var textRunBounds = new List(1); var endX = startX; for (int i = firstRunIndex; i <= lastRunIndex; i++) @@ -957,7 +982,10 @@ namespace Avalonia.Media.TextFormatting { var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); - textRunBounds.Add(runBounds); + if(runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length) + { + textRunBounds.Add(runBounds); + } if (offset > 0) { @@ -976,20 +1004,26 @@ namespace Avalonia.Media.TextFormatting } else { - if (currentRun is DrawableTextRun drawableTextRun) + if (currentPosition < FirstTextSourceIndex + Length) { - textRunBounds.Add( - new TextRunBounds( - new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun)); + if (currentRun is DrawableTextRun drawableTextRun) + { + var runBounds = new TextRunBounds( + new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun); - endX += drawableTextRun.Size.Width; - } - else - { - //Add potential TextEndOfParagraph - textRunBounds.Add( - new TextRunBounds( - new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun)); + textRunBounds.Add(runBounds); + + + endX += drawableTextRun.Size.Width; + } + else + { + //Add potential TextEndOfParagraph + var runBounds = new TextRunBounds( + new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun); + + textRunBounds.Add(runBounds); + } } currentPosition += currentRun.Length; @@ -1032,6 +1066,20 @@ namespace Avalonia.Media.TextFormatting startIndex += offset; } + //Make sure we start the hit test at the start of the possible cluster. + var clusterStartHit = currentRun.GlyphRun.GetPreviousCaretCharacterHit(new CharacterHit(startIndex)); + var clusterEndHit = currentRun.GlyphRun.GetNextCaretCharacterHit(clusterStartHit); + + var clusterOffset = 0; + + if (startIndex > clusterStartHit.FirstCharacterIndex && startIndex < clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength) + { + clusterOffset = clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength - startIndex; + + //We need to move the startIndex to the start of the cluster. + startIndex -= clusterOffset; + } + var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); @@ -1041,7 +1089,8 @@ namespace Avalonia.Media.TextFormatting var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); + //Adjust characterLength by the cluster offset to only cover the remaining length of the cluster. + var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset; if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length) { @@ -1075,7 +1124,7 @@ namespace Avalonia.Media.TextFormatting var runWidth = endX - startX; - var textSourceIndex = offset + startHit.FirstCharacterIndex; + var textSourceIndex = startIndex + Math.Max(0, currentPosition - firstCluster) + clusterOffset; return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun); } @@ -1101,7 +1150,6 @@ namespace Avalonia.Media.TextFormatting } var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); startX -= currentRun.Size.Width - startOffset; @@ -1110,7 +1158,21 @@ namespace Avalonia.Media.TextFormatting var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); - var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); + //Make sure we start the hit test at the start of the possible cluster. + var clusterStartHit = currentRun.GlyphRun.GetNextCaretCharacterHit(new CharacterHit(startIndex)); + var clusterEndHit = currentRun.GlyphRun.GetPreviousCaretCharacterHit(startHit); + + var clusterOffset = 0; + + if (startIndex > clusterStartHit.FirstCharacterIndex && startIndex < clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength) + { + clusterOffset = clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength - startIndex; + + //We need to move the startIndex to the start of the cluster. + startIndex -= clusterOffset; + } + + var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset; if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length) { @@ -1149,7 +1211,7 @@ namespace Avalonia.Media.TextFormatting var runWidth = endX - startX; - var textSourceIndex = offset + startHit.FirstCharacterIndex; + var textSourceIndex = startIndex + Math.Max(0, currentPosition - firstCluster) + clusterOffset; return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun); } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 6f8b2c407f..dcf285263e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -886,6 +886,322 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Throw_ArgumentOutOfRangeException_For_Zero_TextLength() + { + using (Start()) + { + var typeface = Typeface.Default; + + var defaultProperties = new GenericTextRunProperties(typeface); + var textSource = new CustomTextBufferTextSource(new TextCharacters("1234", defaultProperties)); + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.NotNull(textLine); + + Assert.Throws(() => textLine.GetTextBounds(0, 0)); + } + } + + [Fact] + public void Should_GetTextBounds_For_Negative_TextLength() + { + using (Start()) + { + var typeface = Typeface.Default; + + var defaultProperties = new GenericTextRunProperties(typeface); + var textSource = new CustomTextBufferTextSource(new TextCharacters("1234", defaultProperties)); + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.NotNull(textLine); + + var textBounds = textLine.GetTextBounds(0, -1); + + Assert.NotNull(textBounds); + + Assert.NotEmpty(textBounds); + + var firstBounds = textBounds[0]; + + Assert.Empty(firstBounds.TextRunBounds); + + Assert.Equal(0, firstBounds.Rectangle.Width); + + Assert.Equal(0, firstBounds.Rectangle.Left); + } + } + + [Fact] + public void Should_GetTextBounds_For_Exceeding_TextLength() + { + using (Start()) + { + var typeface = Typeface.Default; + + var defaultProperties = new GenericTextRunProperties(typeface); + var textSource = new CustomTextBufferTextSource(new TextCharacters("1234", defaultProperties)); + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.NotNull(textLine); + + var textBounds = textLine.GetTextBounds(10, 1); + + Assert.NotNull(textBounds); + + Assert.NotEmpty(textBounds); + + var firstBounds = textBounds[0]; + + Assert.Empty(firstBounds.TextRunBounds); + + Assert.Equal(0, firstBounds.Rectangle.Width); + + Assert.Equal(textLine.WidthIncludingTrailingWhitespace, firstBounds.Rectangle.Right); + } + } + + [Fact] + public void Should_GetTextBounds_For_Mixed_Hidden_Runs_With_Ligature() + { + using (Start()) + { + var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope")); + + var defaultProperties = new GenericTextRunProperties(typeface); + var textSource = new CustomTextBufferTextSource( + new TextHidden(1), + new TextCharacters("Authenti", defaultProperties), + new TextHidden(1), + new TextHidden(1), + new TextCharacters("ff", defaultProperties), + new TextHidden(1), + new TextHidden(1)); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.NotNull(textLine); + + var textBounds = textLine.GetTextBounds(12, 1); + + Assert.NotEmpty(textBounds); + + var firstBounds = textBounds[0]; + + Assert.NotNull(firstBounds.TextRunBounds); + Assert.NotEmpty(firstBounds.TextRunBounds); + + var firstRun = firstBounds.TextRunBounds[0]; + + Assert.NotNull(firstRun); + + Assert.Equal(12, firstRun.TextSourceCharacterIndex); + } + } + + [Fact] + public void Should_GetTextBounds_For_Mixed_Hidden_Runs() + { + using (Start()) + { + var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope")); + + var defaultProperties = new GenericTextRunProperties(typeface); + var textSource = new CustomTextBufferTextSource( + new TextHidden(1), + new TextCharacters("Authenti", defaultProperties), + new TextHidden(1), + new TextHidden(1), + new TextEndOfParagraph(1)); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.NotNull(textLine); + + var textBounds = textLine.GetTextBounds(8, 1); + + Assert.NotEmpty(textBounds); + + var firstBounds = textBounds[0]; + + Assert.NotNull(firstBounds.TextRunBounds); + Assert.NotEmpty(firstBounds.TextRunBounds); + + var firstRun = firstBounds.TextRunBounds[0]; + + Assert.NotNull(firstRun); + + Assert.Equal(8, firstRun.TextSourceCharacterIndex); + } + } + + [Win32Fact("Windows font")] + public void Should_GetTextBounds_Within_Cluster() + { + using (Start()) + { + var typeface = new Typeface("Segoe UI Emoji"); + + var defaultProperties = new GenericTextRunProperties(typeface); + var textSource = new CustomTextBufferTextSource(new TextCharacters("🙈", defaultProperties)); + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.NotNull(textLine); + + var textBounds = textLine.GetTextBounds(0, 1); + + Assert.NotEmpty(textBounds); + + var runBounds = textBounds[0].TextRunBounds[0]; + + Assert.Equal(0, runBounds.TextSourceCharacterIndex); + + textBounds = textLine.GetTextBounds(1, 1); + + Assert.NotEmpty(textBounds); + + runBounds = textBounds[0].TextRunBounds[0]; + + Assert.Equal(1, runBounds.TextSourceCharacterIndex); + + textBounds = textLine.GetTextBounds(2, 1); + + Assert.NotEmpty(textBounds); + + Assert.NotNull(textBounds[0].TextRunBounds); + + Assert.Empty(textBounds[0].TextRunBounds); + } + } + + [Win32Fact("Windows font")] + public void Should_GetTextBounds_After_Last_Index() + { + using (Start()) + { + var typeface = new Typeface("Segoe UI Emoji"); + + var defaultProperties = new GenericTextRunProperties(typeface); + var textSource = new CustomTextBufferTextSource(new TextCharacters("🙈", defaultProperties)); + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.NotNull(textLine); + + var textBounds = textLine.GetTextBounds(2, 1); + + Assert.NotEmpty(textBounds); + + var firstBounds = textBounds[0]; + + Assert.Equal(textLine.Width, firstBounds.Rectangle.Right); + + Assert.NotNull(firstBounds.TextRunBounds); + + Assert.Empty(firstBounds.TextRunBounds); + } + } + + [Fact] + public void Should_Get_Run_Bounds() + { + using (Start()) + { + var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope")); + var defaultProperties = new GenericTextRunProperties(typeface); + var textSource = new CustomTextBufferTextSource( + new TextCharacters("He", defaultProperties), + new TextCharacters("Wo", defaultProperties), + new TextCharacters("ff", defaultProperties)); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.NotNull(textLine); + + var textBounds = textLine.GetTextBounds(1, 1); + + Assert.NotEmpty(textBounds); + + textBounds = textLine.GetTextBounds(2, 1); + + Assert.NotEmpty(textBounds); + + textBounds = textLine.GetTextBounds(4, 1); + + Assert.NotEmpty(textBounds); + } + } + + private class TextHidden : TextRun + { + public TextHidden(int length) + { + Length = length; + } + + public override int Length { get; } + } + + private class CustomTextBufferTextSource : ITextSource + { + private IReadOnlyList _textRuns; + + public CustomTextBufferTextSource(params TextRun[] textRuns) + { + _textRuns = textRuns; + } + + public TextRun? GetTextRun(int textSourceIndex) + { + var pos = 0; + + for(var i = 0; i < _textRuns.Count; i++) + { + var currentRun = _textRuns[i]; + + if(pos + currentRun.Length > textSourceIndex) + { + return currentRun; + } + + pos += currentRun.Length; + } + + return null; + } + } + private class MixedTextBufferTextSource : ITextSource { public TextRun? GetTextRun(int textSourceIndex)