diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt
new file mode 100644
index 0000000000..5058cff26d
--- /dev/null
+++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt
@@ -0,0 +1,5 @@
+Compat issues with assembly Avalonia.Visuals:
+CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak' is abstract in the implementation but is missing in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.LineBreak.get()' does not exist in the implementation but it does exist in the contract.
+CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak.get()' is abstract in the implementation but is missing in the contract.
+Total Issues: 3
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
index fa7d6cb4bf..df1ecb4067 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
@@ -221,7 +221,7 @@ namespace Avalonia.Media.TextFormatting
while (currentPosition < _text.Length)
{
var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth,
- _paragraphProperties, previousLine?.LineBreak);
+ _paragraphProperties, previousLine?.TextLineBreak);
currentPosition += textLine.TextRange.Length;
@@ -230,7 +230,7 @@ namespace Avalonia.Media.TextFormatting
if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) &&
height + textLine.LineMetrics.Size.Height > MaxHeight)
{
- if (previousLine?.LineBreak != null && _textTrimming != TextTrimming.None)
+ if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
{
var collapsedLine =
previousLine.Collapse(GetCollapsingProperties(MaxWidth));
@@ -255,7 +255,7 @@ namespace Avalonia.Media.TextFormatting
previousLine = textLine;
- if (currentPosition != _text.Length || textLine.LineBreak == null)
+ if (currentPosition != _text.Length || textLine.TextLineBreak == null)
{
continue;
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
index 423ca9fb7f..c052fb8948 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
@@ -35,9 +35,9 @@ namespace Avalonia.Media.TextFormatting
/// Gets the state of the line when broken by line breaking process.
///
///
- /// A value that represents the line break.
+ /// A value that represents the line break.
///
- public abstract TextLineBreak LineBreak { get; }
+ public abstract TextLineBreak TextLineBreak { get; }
///
/// Gets a value that indicates whether the line is collapsed.
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
index 08d9107bb1..51092cddda 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
@@ -13,7 +13,7 @@ namespace Avalonia.Media.TextFormatting
{
_textRuns = textRuns;
LineMetrics = lineMetrics;
- LineBreak = lineBreak;
+ TextLineBreak = lineBreak;
HasCollapsed = hasCollapsed;
}
@@ -27,7 +27,7 @@ namespace Avalonia.Media.TextFormatting
public override TextLineMetrics LineMetrics { get; }
///
- public override TextLineBreak LineBreak { get; }
+ public override TextLineBreak TextLineBreak { get; }
///
public override bool HasCollapsed { get; }
@@ -122,7 +122,7 @@ namespace Avalonia.Media.TextFormatting
textLineMetrics = new TextLineMetrics(new Size(shapedWidth, LineMetrics.Size.Height),
LineMetrics.TextBaseline, textRange, false);
- return new TextLineImpl(shapedTextCharacters, textLineMetrics, LineBreak, true);
+ return new TextLineImpl(shapedTextCharacters, textLineMetrics, TextLineBreak, true);
}
availableWidth -= currentRun.GlyphRun.Bounds.Width;
@@ -268,6 +268,17 @@ namespace Avalonia.Media.TextFormatting
var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength ==
TextRange.Length;
+ var characterIndex = codepointIndex - run.Text.Start;
+
+ var codepoint = Codepoint.ReadAt(run.GlyphRun.Characters, characterIndex, out _);
+
+ if (codepoint.IsBreakChar)
+ {
+ foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(codepointIndex - 1, out _);
+
+ isAtEnd = true;
+ }
+
nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
foundCharacterHit :
new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index 7abfe29f11..8f1bd5979a 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
@@ -331,6 +333,30 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
+ [Fact]
+ public void Should_Ignore_Invisible_Characters()
+ {
+ using (Start())
+ {
+ var defaultTextRunProperties =
+ new GenericTextRunProperties(Typeface.Default);
+
+ const string text = "01234567🎉\n";
+
+ var source = new SingleBufferTextSource(text, defaultTextRunProperties);
+
+ var textParagraphProperties = new GenericTextParagraphProperties(defaultTextRunProperties);
+
+ var formatter = TextFormatter.Current;
+
+ var textLine = formatter.FormatLine(source, 0, double.PositiveInfinity, textParagraphProperties);
+
+ var nextCharacterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(8, 2));
+
+ Assert.Equal(new CharacterHit(8, 2), nextCharacterHit);
+ }
+ }
+
private static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface