From 250743d786147434b36c1f584d7741964f3e4975 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 18 Jul 2023 09:40:03 +0200 Subject: [PATCH] Rework GetNext/PreviousCharacterHit --- src/Avalonia.Base/Media/GlyphRun.cs | 6 +- .../Media/TextFormatting/TextLineImpl.cs | 321 ++++++------------ .../Media/GlyphRunTests.cs | 2 +- .../Media/TextFormatting/TextLineTests.cs | 116 ++++++- 4 files changed, 222 insertions(+), 223 deletions(-) diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 0f70386424..fcb2cec733 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -424,13 +424,13 @@ namespace Avalonia.Media /// public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) { + var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); + if (characterHit.TrailingLength != 0) { - return new CharacterHit(characterHit.FirstCharacterIndex); + return previousCharacterHit; } - var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); - return new CharacterHit(previousCharacterHit.FirstCharacterIndex); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index ca31d9a6d0..44f53420de 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -9,7 +9,7 @@ namespace Avalonia.Media.TextFormatting internal static Comparer TextBoundsComparer { get; } = Comparer.Create((x, y) => x.Rectangle.Left.CompareTo(y.Rectangle.Left)); - private IReadOnlyList? _indexedTextRuns; + internal IReadOnlyList? _indexedTextRuns; private readonly TextRun[] _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; @@ -512,38 +512,45 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) { - if (_textRuns.Length == 0) + if (_textRuns.Length == 0 || _indexedTextRuns is null) { return new CharacterHit(); } - if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit)) - { - return nextCharacterHit; - } - - var lastTextPosition = FirstTextSourceIndex + Length; + var currentCharacterrHit = characterHit; + var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - // Can't move, we're after the last character - var runIndex = GetRunIndexAtCharacterIndex(lastTextPosition, LogicalDirection.Forward, out var currentPosition); + var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Forward, out var currentPosition); - var currentRun = _textRuns[runIndex]; + var nextCharacterHit = characterHit; switch (currentRun) { case ShapedTextRun shapedRun: { - nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit); + var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster - characterHit.TrailingLength); + + if (offset > 0) + { + currentCharacterrHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength); + } + + nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(currentCharacterrHit); + + if (offset > 0) + { + nextCharacterHit = new CharacterHit(nextCharacterHit.FirstCharacterIndex + offset, nextCharacterHit.TrailingLength); + } break; } - default: + case TextRun: { nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length); break; } } - if (characterHit.FirstCharacterIndex + characterHit.TrailingLength == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength) + if (characterIndex == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength) { return characterHit; } @@ -554,17 +561,75 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) { - if (TryFindPreviousCharacterHit(characterHit, out var previousCharacterHit)) + if (_textRuns.Length == 0 || _indexedTextRuns is null) + { + return new CharacterHit(); + } + + if (characterHit.TrailingLength > 0 && characterHit.FirstCharacterIndex <= FirstTextSourceIndex) + { + return new CharacterHit(FirstTextSourceIndex); + } + + var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + if (characterIndex <= FirstTextSourceIndex) { - return previousCharacterHit; + return new CharacterHit(FirstTextSourceIndex); + } + + var currentCharacterrHit = characterHit; + + var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition); + + if (currentPosition == characterHit.FirstCharacterIndex) + { + currentRun = GetRunAtCharacterIndex(characterHit.FirstCharacterIndex, LogicalDirection.Backward, out currentPosition); + } + + var previousCharacterHit = characterHit; + + switch (currentRun) + { + case ShapedTextRun shapedRun: + { + var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster); + + if (offset > 0) + { + currentCharacterrHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength); + } + + previousCharacterHit = shapedRun.GlyphRun.GetPreviousCaretCharacterHit(currentCharacterrHit); + + if (offset > 0) + { + previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + offset, previousCharacterHit.TrailingLength); + } + break; + } + case TextRun: + { + if (characterHit.TrailingLength > 0) + { + previousCharacterHit = new CharacterHit(currentPosition, currentRun.Length); + + } + else + { + previousCharacterHit = new CharacterHit(currentPosition + currentRun.Length); + } + + break; + } } - if (characterHit.FirstCharacterIndex <= FirstTextSourceIndex) + if (characterIndex == previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength) { - characterHit = new CharacterHit(FirstTextSourceIndex); + return characterHit; } - return characterHit; // Can't move, we're before the first character + return previousCharacterHit; } /// @@ -1009,161 +1074,7 @@ namespace Avalonia.Media.TextFormatting if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine) { _textLineBreak = new TextLineBreak(textEndOfLine); - } - } - - /// - /// Tries to find the next character hit. - /// - /// The current character hit. - /// The next character hit. - /// - private bool TryFindNextCharacterHit(CharacterHit characterHit, out CharacterHit nextCharacterHit) - { - nextCharacterHit = characterHit; - - var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - var lastCodepointIndex = FirstTextSourceIndex + Length; - - if (codepointIndex >= lastCodepointIndex) - { - return false; // Cannot go forward anymore - } - - if (codepointIndex < FirstTextSourceIndex) - { - codepointIndex = FirstTextSourceIndex; - } - - var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition); - - while (runIndex < _textRuns.Length) - { - var currentRun = _textRuns[runIndex]; - - switch (currentRun) - { - case ShapedTextRun shapedRun: - { - var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); - - var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == FirstTextSourceIndex + Length; - - if (isAtEnd && !shapedRun.GlyphRun.IsLeftToRight) - { - nextCharacterHit = foundCharacterHit; - - return true; - } - - nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ? - foundCharacterHit : - new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength); - - if (isAtEnd || nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex) - { - return true; - } - - break; - } - default: - { - var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - - if (textPosition == currentPosition) - { - nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length); - - return true; - } - - break; - } - } - - currentPosition += currentRun.Length; - runIndex++; - } - - return false; - } - - /// - /// Tries to find the previous character hit. - /// - /// The current character hit. - /// The previous character hit. - /// - private bool TryFindPreviousCharacterHit(CharacterHit characterHit, out CharacterHit previousCharacterHit) - { - var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - - if (characterIndex == FirstTextSourceIndex) - { - previousCharacterHit = new CharacterHit(FirstTextSourceIndex); - - return true; - } - - previousCharacterHit = characterHit; - - if (characterIndex < FirstTextSourceIndex) - { - return false; // Cannot go backward anymore. - } - - var runIndex = GetRunIndexAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition); - - while (runIndex >= 0) - { - var currentRun = _textRuns[runIndex]; - - switch (currentRun) - { - case ShapedTextRun shapedRun: - { - var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); - - if (foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength < characterIndex) - { - previousCharacterHit = foundCharacterHit; - - return true; - } - - var previousPosition = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength; - - if (foundCharacterHit.TrailingLength > 0 && previousPosition == characterIndex) - { - previousCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex); - } - - if (previousCharacterHit != characterHit) - { - return true; - } - - break; - } - default: - { - if (characterIndex == currentPosition + currentRun.Length) - { - previousCharacterHit = new CharacterHit(currentPosition); - - return true; - } - - break; - } - } - - currentPosition -= currentRun.Length; - runIndex--; } - - return false; } /// @@ -1173,15 +1084,23 @@ namespace Avalonia.Media.TextFormatting /// The logical direction. /// The text position of the found run index. /// The text run index. - private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition) + private TextRun? GetRunAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition) { var runIndex = 0; textPosition = FirstTextSourceIndex; + + if (_indexedTextRuns is null) + { + return null; + } + + TextRun? currentRun = null; TextRun? previousRun = null; - while (runIndex < _textRuns.Length) + while (runIndex < _indexedTextRuns.Count) { - var currentRun = _textRuns[runIndex]; + var indexedRun = _indexedTextRuns[runIndex]; + currentRun = indexedRun.TextRun; switch (currentRun) { @@ -1189,64 +1108,49 @@ namespace Avalonia.Media.TextFormatting { var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster; - if (firstCluster > codepointIndex) - { - break; - } - - if (previousRun is ShapedTextRun previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight) - { - if (shapedRun.ShapedBuffer.IsLeftToRight) - { - if (firstCluster >= codepointIndex) - { - return --runIndex; - } - } - else - { - if (codepointIndex > firstCluster + currentRun.Length) - { - return --runIndex; - } - } - } + firstCluster += Math.Max(0, indexedRun.TextSourceCharacterIndex - firstCluster); if (direction == LogicalDirection.Forward) { - if (codepointIndex >= firstCluster && codepointIndex <= firstCluster + currentRun.Length) + if (codepointIndex >= firstCluster && codepointIndex < firstCluster + currentRun.Length) { - return runIndex; + return currentRun; } } else { - if (codepointIndex > firstCluster && - codepointIndex <= firstCluster + currentRun.Length) + if (previousRun is not null && previousRun is not ShapedTextRun && codepointIndex == textPosition + firstCluster) + { + textPosition -= previousRun.Length; + + return previousRun; + } + + if (codepointIndex > firstCluster && codepointIndex <= firstCluster + currentRun.Length) { - return runIndex; + return currentRun; } } if (runIndex + 1 >= _textRuns.Length) { - return runIndex; + return currentRun; } textPosition += currentRun.Length; break; } - default: + case TextRun: { if (codepointIndex == textPosition) { - return runIndex; + return currentRun; } if (runIndex + 1 >= _textRuns.Length) { - return runIndex; + return currentRun; } textPosition += currentRun.Length; @@ -1257,10 +1161,11 @@ namespace Avalonia.Media.TextFormatting } runIndex++; + previousRun = currentRun; } - return runIndex; + return currentRun; } private TextLineMetrics CreateLineMetrics() diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index c273cc6489..69d7fc4916 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -111,7 +111,7 @@ namespace Avalonia.Base.UnitTests.Media using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel)) { - var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex, currentLength)); + var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex + currentLength)); Assert.Equal(previousIndex, characterHit.FirstCharacterIndex); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 12427e1f9e..d576a64523 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -194,7 +194,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting for (var i = 0; i < clusters.Count; i++) { var expectedCluster = clusters[i]; - var actualCluster = nextCharacterHit.FirstCharacterIndex; + var actualCluster = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength; Assert.Equal(expectedCluster, actualCluster); @@ -278,16 +278,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(clusters[i], previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength); } - - firstCharacterHit = previousCharacterHit; - - firstCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit); - - previousCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit); - - Assert.Equal(firstCharacterHit.FirstCharacterIndex, previousCharacterHit.FirstCharacterIndex); - - Assert.Equal(0, previousCharacterHit.TrailingLength); } } @@ -728,6 +718,110 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_GetNextCaretCharacterHit_From_Mixed_TextBuffer() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var textSource = new MixedTextBufferTextSource(); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(9, 1)); + + Assert.Equal(10, characterHit.FirstCharacterIndex); + + Assert.Equal(1, characterHit.TrailingLength); + + characterHit = textLine.GetNextCaretCharacterHit(characterHit); + + Assert.Equal(11, characterHit.FirstCharacterIndex); + + Assert.Equal(1, characterHit.TrailingLength); + + characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(19, 1)); + + Assert.Equal(20, characterHit.FirstCharacterIndex); + + Assert.Equal(1, characterHit.TrailingLength); + + characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(10)); + + Assert.Equal(11, characterHit.FirstCharacterIndex); + + Assert.Equal(0, characterHit.TrailingLength); + + characterHit = textLine.GetNextCaretCharacterHit(characterHit); + + Assert.Equal(12, characterHit.FirstCharacterIndex); + + Assert.Equal(0, characterHit.TrailingLength); + + characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(20)); + + Assert.Equal(21, characterHit.FirstCharacterIndex); + + Assert.Equal(0, characterHit.TrailingLength); + } + } + + [Fact] + public void Should_GetPreviousCaretCharacterHit_From_Mixed_TextBuffer() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var textSource = new MixedTextBufferTextSource(); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(20, 1)); + + Assert.Equal(19, characterHit.FirstCharacterIndex); + + Assert.Equal(1, characterHit.TrailingLength); + + characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(10, 1)); + + Assert.Equal(9, characterHit.FirstCharacterIndex); + + Assert.Equal(1, characterHit.TrailingLength); + + characterHit = textLine.GetPreviousCaretCharacterHit(characterHit); + + Assert.Equal(8, characterHit.FirstCharacterIndex); + + Assert.Equal(1, characterHit.TrailingLength); + + characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(21)); + + Assert.Equal(20, characterHit.FirstCharacterIndex); + + Assert.Equal(0, characterHit.TrailingLength); + + characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(11)); + + Assert.Equal(10, characterHit.FirstCharacterIndex); + + Assert.Equal(0, characterHit.TrailingLength); + + characterHit = textLine.GetPreviousCaretCharacterHit(characterHit); + + Assert.Equal(9, characterHit.FirstCharacterIndex); + + Assert.Equal(0, characterHit.TrailingLength); + } + } + private class MixedTextBufferTextSource : ITextSource { public TextRun? GetTextRun(int textSourceIndex)