diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 24ddfa884a..4a082b0246 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -354,8 +354,12 @@ namespace Avalonia.Controls if (!_ignoreTextChanges) { var caretIndex = CaretIndex; + var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; CaretIndex = CoerceCaretIndex(caretIndex, value); + SelectionStart = CoerceCaretIndex(selectionStart, value); + SelectionEnd = CoerceCaretIndex(selectionEnd, value); if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs index 9d648af8fb..b31a6f4d13 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs @@ -22,5 +22,41 @@ namespace Avalonia.Media.TextFormatting public override TextRunProperties Properties { get; } public sbyte BidiLevel { get; } + + public bool CanShapeTogether(ShapeableTextCharacters shapeableTextCharacters) + { + if (!Text.Buffer.Equals(shapeableTextCharacters.Text.Buffer)) + { + return false; + } + + if (Text.Start + Text.Length != shapeableTextCharacters.Text.Start) + { + return false; + } + + if (BidiLevel != shapeableTextCharacters.BidiLevel) + { + return false; + } + + if (!MathUtilities.AreClose(Properties.FontRenderingEmSize, + shapeableTextCharacters.Properties.FontRenderingEmSize)) + { + return false; + } + + if (Properties.Typeface != shapeableTextCharacters.Properties.Typeface) + { + return false; + } + + if (Properties.BaselineAlignment != shapeableTextCharacters.Properties.BaselineAlignment) + { + return false; + } + + return true; + } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs index 750ec64798..c4b2dfb3a5 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs @@ -176,6 +176,12 @@ namespace Avalonia.Media.TextFormatting var currentScript = currentGrapheme.FirstCodepoint.Script; + //Stop at the first missing glyph + if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) + { + break; + } + if (currentScript != script) { if (script is Script.Unknown || currentScript != Script.Common && @@ -192,12 +198,6 @@ namespace Avalonia.Media.TextFormatting } } - //Stop at the first missing glyph - if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) - { - break; - } - length += currentGrapheme.Text.Length; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index d687fb25ed..13ed850715 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; @@ -171,25 +173,83 @@ namespace Avalonia.Media.TextFormatting resolvedFlowDirection = (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft; - foreach (var shapeableRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels)) + var shapeableRuns = new List(textRuns.Count); + + foreach (var coalescedRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels)) + { + shapeableRuns.AddRange(coalescedRuns); + } + + for (var index = 0; index < shapeableRuns.Count; index++) { - for (var index = 0; index < shapeableRuns.Count; index++) + var currentRun = shapeableRuns[index]; + var groupedRuns = new List(2) { currentRun }; + var text = currentRun.Text; + var start = currentRun.Text.Start; + var length = currentRun.Text.Length; + var bufferOffset = currentRun.Text.BufferOffset; + + while (index + 1 < shapeableRuns.Count) { - var currentRun = shapeableRuns[index]; + var nextRun = shapeableRuns[index + 1]; - var shapedBuffer = TextShaper.Current.ShapeText(currentRun.Text, currentRun.Properties.Typeface.GlyphTypeface, - currentRun.Properties.FontRenderingEmSize, currentRun.Properties.CultureInfo, currentRun.BidiLevel); + if (currentRun.CanShapeTogether(nextRun)) + { + groupedRuns.Add(nextRun); - var shapedCharacters = new ShapedTextCharacters(shapedBuffer, currentRun.Properties); + length += nextRun.Text.Length; + + if (start > nextRun.Text.Start) + { + start = nextRun.Text.Start; + } + if (bufferOffset > nextRun.Text.BufferOffset) + { + bufferOffset = nextRun.Text.BufferOffset; + } + + text = new ReadOnlySlice(text.Buffer, start, length, bufferOffset); + + index++; - shapedTextCharacters.Add(shapedCharacters); + currentRun = nextRun; + + continue; + } + + break; } + + shapedTextCharacters.AddRange(ShapeTogether(groupedRuns, text)); } return shapedTextCharacters; } + private static IReadOnlyList ShapeTogether( + IReadOnlyList textRuns, ReadOnlySlice text) + { + var shapedRuns = new List(textRuns.Count); + var firstRun = textRuns[0]; + + var shapedBuffer = TextShaper.Current.ShapeText(text, firstRun.Properties.Typeface.GlyphTypeface, + firstRun.Properties.FontRenderingEmSize, firstRun.Properties.CultureInfo, firstRun.BidiLevel); + + for (var i = 0; i < textRuns.Count; i++) + { + var currentRun = textRuns[i]; + + var splitResult = shapedBuffer.Split(currentRun.Text.Length); + + shapedRuns.Add(new ShapedTextCharacters(splitResult.First, currentRun.Properties)); + + shapedBuffer = splitResult.Second!; + } + + return shapedRuns; + } + /// /// Coalesces ranges of the same bidi level to form /// diff --git a/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs b/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs index ee85d1e876..ad545b2923 100644 --- a/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs +++ b/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs @@ -13,7 +13,7 @@ namespace Avalonia.Utilities [DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))] public readonly struct ReadOnlySlice : IReadOnlyList where T : struct { - private readonly int _offset; + private readonly int _bufferOffset; /// /// Gets an empty @@ -24,7 +24,7 @@ namespace Avalonia.Utilities public ReadOnlySlice(ReadOnlyMemory buffer) : this(buffer, 0, buffer.Length) { } - public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length, int offset = 0) + public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length, int bufferOffset = 0) { #if DEBUG if (start.CompareTo(0) < 0) @@ -41,7 +41,7 @@ namespace Avalonia.Utilities _buffer = buffer; Start = start; Length = length; - _offset = offset; + _bufferOffset = bufferOffset; } /// @@ -74,12 +74,17 @@ namespace Avalonia.Utilities public bool IsEmpty => Length == 0; /// - /// The underlying span. + /// Get the underlying span. /// - public ReadOnlySpan Span => _buffer.Span.Slice(_offset, Length); + public ReadOnlySpan Span => _buffer.Span.Slice(_bufferOffset, Length); /// - /// The underlying buffer. + /// Get the buffer offset. + /// + public int BufferOffset => _bufferOffset; + + /// + /// Get the underlying buffer. /// public ReadOnlyMemory Buffer => _buffer; @@ -124,17 +129,17 @@ namespace Avalonia.Utilities return Empty; } - if (start < 0 || _offset + start > _buffer.Length - 1) + if (start < 0 || _bufferOffset + start > _buffer.Length - 1) { throw new ArgumentOutOfRangeException(nameof(start)); } - if (_offset + start + length > _buffer.Length) + if (_bufferOffset + start + length > _buffer.Length) { throw new ArgumentOutOfRangeException(nameof(length)); } - return new ReadOnlySlice(_buffer, start, length, _offset); + return new ReadOnlySlice(_buffer, start, length, _bufferOffset); } /// @@ -154,7 +159,7 @@ namespace Avalonia.Utilities throw new ArgumentOutOfRangeException(nameof(length)); } - return new ReadOnlySlice(_buffer, Start, length, _offset); + return new ReadOnlySlice(_buffer, Start, length, _bufferOffset); } /// @@ -174,7 +179,7 @@ namespace Avalonia.Utilities throw new ArgumentOutOfRangeException(nameof(length)); } - return new ReadOnlySlice(_buffer, Start + length, Length - length, _offset + length); + return new ReadOnlySlice(_buffer, Start + length, Length - length, _bufferOffset + length); } /// diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index c4d11f4613..f7222446af 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -69,7 +69,7 @@ namespace Avalonia.Skia return shapedBuffer; } } - + private static void MergeBreakPair(Buffer buffer) { var length = buffer.Length;