diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index d6e7375772..417dfc77fa 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -853,8 +853,13 @@ namespace Avalonia.Media.TextFormatting { var length = 0; - while (Codepoint.ReadAt(shapedRun.GlyphRun.Characters.Span, length, out var count) != Codepoint.ReplacementCodepoint) + while (Codepoint.ReadAt(shapedRun.GlyphRun.Characters.Span, length, out var count) is Codepoint codepoint && codepoint != Codepoint.ReplacementCodepoint) { + if (codepoint.Value == 0x0D && Codepoint.ReadAt(shapedRun.GlyphRun.Characters.Span, length + count, out var lfCount).Value == 0x0A) + { + count += lfCount; + } + if (length + count >= runOffset) { break; @@ -869,7 +874,7 @@ namespace Avalonia.Media.TextFormatting { previousCharacterHit = shapedRun.GlyphRun.GetPreviousCaretCharacterHit(new CharacterHit(firstCluster + runOffset)); - if(textSourceOffset > 0) + if (textSourceOffset > 0) { previousCharacterHit = new CharacterHit(textSourceOffset + previousCharacterHit.FirstCharacterIndex, previousCharacterHit.TrailingLength); } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 4286904d4d..eaf0dac909 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -174,6 +174,12 @@ namespace Avalonia.Headless var codepoint = Codepoint.ReadAt(textSpan, i, out var count); + // Handle CRLF as a single cluster + if (codepoint.Value == 0x0D && Codepoint.ReadAt(textSpan, i + count, out var lfCount).Value == 0x0A) + { + count += lfCount; + } + var glyphIndex = typeface.GetGlyph(codepoint); for (var j = 0; j < count; ++j) @@ -218,7 +224,7 @@ namespace Avalonia.Headless return false; } - public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { glyphTypeface = null; @@ -238,9 +244,9 @@ namespace Avalonia.Headless public virtual bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, out IGlyphTypeface glyphTypeface) { glyphTypeface = new HeadlessGlyphTypefaceImpl( - FontFamily.DefaultFontFamilyName, - fontSimulations.HasFlag(FontSimulations.Oblique) ? FontStyle.Italic : FontStyle.Normal, - fontSimulations.HasFlag(FontSimulations.Bold) ? FontWeight.Bold : FontWeight.Normal, + FontFamily.DefaultFontFamilyName, + fontSimulations.HasFlag(FontSimulations.Oblique) ? FontStyle.Italic : FontStyle.Normal, + fontSimulations.HasFlag(FontSimulations.Bold) ? FontWeight.Bold : FontWeight.Normal, FontStretch.Normal); TryCreateGlyphTypefaceCount++; diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 2e94b1608f..c20943807e 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -2129,6 +2129,25 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Backspace_Should_Delete_CRLFNewline_Character_At_Once() + { + using var _ = UnitTestApplication.Start(Services); + var target = new TextBox + { + Template = CreateTemplate(), + Text = $"First\r\nSecond", + CaretIndex = 7 + }; + target.ApplyTemplate(); + + // (First\r\nSecond) + RaiseKeyEvent(target, Key.Back, KeyModifiers.None); + // (FirstSecond) + + Assert.Equal("FirstSecond", target.Text); + } + private static TestServices FocusServices => TestServices.MockThreadingInterface.With( keyboardDevice: () => new KeyboardDevice(), keyboardNavigation: () => new KeyboardNavigationHandler(), diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index fc298c1201..92a175be78 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -2285,6 +2285,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Backspace_Should_Treat_CRLF_As_A_Unit() + { + 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 SingleBufferTextSource("one\r\n", defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.NotNull(textLine); + + var backspaceHit = textLine.GetBackspaceCaretCharacterHit(new CharacterHit(5)); + + Assert.Equal(3, backspaceHit.FirstCharacterIndex); + } + } + private class FixedRunsTextSource : ITextSource { private readonly IReadOnlyList _textRuns;