diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 98f9568316..10ce31088a 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -394,10 +394,14 @@ namespace Avalonia.Controls.Presenters var x = Math.Floor(_caretBounds.X) + 0.5; var y = Math.Floor(_caretBounds.Y) + 0.5; var b = Math.Ceiling(_caretBounds.Bottom) - 0.5; + + var caretIndex = _lastCharacterHit.FirstCharacterIndex + _lastCharacterHit.TrailingLength; + var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, _lastCharacterHit.TrailingLength > 0); + var textLine = TextLayout.TextLines[lineIndex]; - if (x >= Bounds.Width) + if (_caretBounds.X > 0 && _caretBounds.X >= textLine.WidthIncludingTrailingWhitespace) { - x = Math.Floor(_caretBounds.X - 1) + 0.5; + x -= 1; } return (new Point(x, y), new Point(x, b)); @@ -468,8 +472,8 @@ namespace Avalonia.Controls.Presenters var typeface = new Typeface(FontFamily, FontStyle, FontWeight); - var selectionStart = SelectionStart; - var selectionEnd = SelectionEnd; + var selectionStart = CoerceCaretIndex(SelectionStart); + var selectionEnd = CoerceCaretIndex(SelectionEnd); var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; @@ -512,11 +516,9 @@ namespace Avalonia.Controls.Presenters _textLayout = null; InvalidateArrange(); - - var scale = LayoutHelper.GetLayoutScale(this); - - var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, scale); + var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1); + return new Size(measuredSize.Width, measuredSize.Height); } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 9395896fbc..09f22612de 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -549,10 +549,8 @@ namespace Avalonia.Controls _textLayout = null; InvalidateArrange(); - - var scale = LayoutHelper.GetLayoutScale(this); - var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, scale); + var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1); return new Size(measuredSize.Width, measuredSize.Height).Inflate(padding); } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 6ff89cd09b..995ec142a5 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -257,6 +257,8 @@ namespace Avalonia.Controls UndoRedoState state; if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text) _undoRedoHelper.UpdateLastState(); + + SelectionStart = SelectionEnd = value; } } @@ -301,14 +303,15 @@ namespace Avalonia.Controls { value = CoerceCaretIndex(value); var changed = SetAndRaise(SelectionStartProperty, ref _selectionStart, value); + if (changed) { UpdateCommandStates(); } - - if (value == SelectionEnd) + + if (SelectionEnd == value && CaretIndex != value) { - CaretIndex = SelectionStart; + CaretIndex = value; } } } @@ -329,8 +332,8 @@ namespace Avalonia.Controls { UpdateCommandStates(); } - - if (value == SelectionStart) + + if (SelectionStart == value && CaretIndex != value) { CaretIndex = value; } @@ -352,10 +355,12 @@ namespace Avalonia.Controls if (!_ignoreTextChanges) { var caretIndex = CaretIndex; + var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; - SelectionStart = CoerceCaretIndex(SelectionStart, value); - SelectionEnd = CoerceCaretIndex(SelectionEnd, value); CaretIndex = CoerceCaretIndex(caretIndex, value); + SelectionStart = CoerceCaretIndex(selectionStart, value); + SelectionEnd = CoerceCaretIndex(selectionEnd, value); if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) { @@ -458,7 +463,7 @@ namespace Avalonia.Controls /// public void ClearSelection() { - SelectionStart = SelectionEnd = CaretIndex; + CaretIndex = SelectionStart; } /// @@ -856,6 +861,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; + CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheEndOfDocument)) { @@ -863,6 +869,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; + CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheStartOfLine)) { @@ -870,7 +877,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; - + CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheEndOfLine)) { @@ -878,6 +885,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; + CaretIndex = _presenter.CaretIndex; } else if (Match(keymap.MoveCursorToTheStartOfDocumentWithSelection)) { @@ -950,7 +958,7 @@ namespace Avalonia.Controls } else { - SelectionStart = SelectionEnd = _presenter.CaretIndex; + CaretIndex = _presenter.CaretIndex; } break; @@ -972,7 +980,7 @@ namespace Avalonia.Controls } else { - SelectionStart = SelectionEnd = _presenter.CaretIndex; + CaretIndex = _presenter.CaretIndex; } break; @@ -996,6 +1004,8 @@ namespace Avalonia.Controls SetTextInternal(text.Substring(0, length) + text.Substring(caretIndex)); + + CaretIndex = _presenter.CaretIndex; } SnapshotUndoRedo(); @@ -1070,8 +1080,6 @@ namespace Avalonia.Controls { e.Handled = true; } - - CaretIndex = _presenter.CaretIndex; } protected override void OnPointerPressed(PointerPressedEventArgs e) @@ -1240,6 +1248,7 @@ namespace Avalonia.Controls { var text = Text ?? string.Empty; var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; if (!wholeWord) { @@ -1248,15 +1257,32 @@ namespace Avalonia.Controls return; } - _presenter.MoveCaretHorizontal(direction > 0 ? LogicalDirection.Forward : LogicalDirection.Backward); - if (isSelecting) { + _presenter.MoveCaretToTextPosition(selectionEnd); + + _presenter.MoveCaretHorizontal(direction > 0 ? + LogicalDirection.Forward : + LogicalDirection.Backward); + SelectionEnd = _presenter.CaretIndex; } else { - SelectionStart = SelectionEnd = _presenter.CaretIndex; + if (selectionStart != selectionEnd) + { + _presenter.MoveCaretToTextPosition(direction > 0 ? + Math.Max(selectionStart, selectionEnd) : + Math.Min(selectionStart, selectionEnd)); + } + else + { + _presenter.MoveCaretHorizontal(direction > 0 ? + LogicalDirection.Forward : + LogicalDirection.Backward); + } + + CaretIndex = _presenter.CaretIndex; } } else @@ -1265,14 +1291,19 @@ namespace Avalonia.Controls if (direction > 0) { - offset = StringUtils.NextWord(text, selectionStart) - selectionStart; + offset = StringUtils.NextWord(text, selectionEnd) - selectionEnd; } else { - offset = StringUtils.PreviousWord(text, selectionStart) - selectionStart; + offset = StringUtils.PreviousWord(text, selectionEnd) - selectionEnd; } - SelectionEnd = CaretIndex + offset; + SelectionEnd += offset; + + if (!isSelecting) + { + CaretIndex = SelectionEnd; + } } } @@ -1316,10 +1347,18 @@ namespace Avalonia.Controls else { var textLines = _presenter.TextLayout.TextLines; - var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false); + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, true); var textLine = textLines[lineIndex]; + + if (caretIndex == textLine.TextRange.Start + textLine.TextRange.Length - textLine.NewLineLength && + lineIndex + 1 < textLines.Count) + { + textLine = textLines[++lineIndex]; + } + + var textPosition = textLine.TextRange.Start + textLine.TextRange.Length - textLine.NewLineLength; - _presenter.MoveCaretToTextPosition(textLine.TextRange.Start + textLine.TextRange.Length, true); + _presenter.MoveCaretToTextPosition(textPosition, true); } } @@ -1330,50 +1369,56 @@ namespace Avalonia.Controls { SelectionStart = 0; SelectionEnd = Text?.Length ?? 0; - CaretIndex = SelectionEnd; } private bool DeleteSelection(bool raiseTextChanged = true) { - if (!IsReadOnly) - { - var selectionStart = SelectionStart; - var selectionEnd = SelectionEnd; + if (IsReadOnly) return true; + + var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; - if (selectionStart != selectionEnd) - { - var start = Math.Min(selectionStart, selectionEnd); - var end = Math.Max(selectionStart, selectionEnd); - var text = Text!; - SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged); - CaretIndex = start; - ClearSelection(); - return true; - } - else - { - return false; - } - } - else + if (selectionStart != selectionEnd) { + var start = Math.Min(selectionStart, selectionEnd); + var end = Math.Max(selectionStart, selectionEnd); + var text = Text!; + + SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged); + + _presenter?.MoveCaretToTextPosition(start); + + CaretIndex= start; + + ClearSelection(); + return true; } + + CaretIndex = SelectionStart; + + return false; } private string GetSelection() { var text = Text; + if (string.IsNullOrEmpty(text)) + { return ""; + } + var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; var start = Math.Min(selectionStart, selectionEnd); var end = Math.Max(selectionStart, selectionEnd); + if (start == end || (Text?.Length ?? 0) < end) { return ""; } + return text.Substring(start, end - start); } @@ -1399,9 +1444,11 @@ namespace Avalonia.Controls private void SetSelectionForControlBackspace() { - SelectionStart = CaretIndex; + var selectionStart = CaretIndex; MoveHorizontal(-1, true, false); + + SelectionStart = selectionStart; } private void SetSelectionForControlDelete() @@ -1413,7 +1460,7 @@ namespace Avalonia.Controls SelectionStart = CaretIndex; - MoveHorizontal(1, true, false); + MoveHorizontal(1, true, true); if (SelectionEnd < _text.Length && _text[SelectionEnd] == ' ') { 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/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 8494a7cbd5..4512890f08 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -607,7 +607,7 @@ namespace Avalonia.Media.TextFormatting textLines.Add(textLine); - UpdateBounds(textLine,ref left, ref width, ref height); + UpdateBounds(textLine, ref left, ref width, ref height); previousLine = textLine; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index ff2bbf53da..e776655284 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -553,7 +553,7 @@ namespace Avalonia.Media.TextFormatting out _); var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == - TextRange.Length; + TextRange.Start + TextRange.Length; if (isAtEnd && !run.GlyphRun.IsLeftToRight) { 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); } ///