Browse Source

Merge pull request #7693 from Gillibald/fixes/textProcessingBugs

Fixes text processing bugs
pull/7754/head
Benedikt Stebner 4 years ago
committed by GitHub
parent
commit
d0f3fedbb6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  2. 4
      src/Avalonia.Controls/TextBlock.cs
  3. 135
      src/Avalonia.Controls/TextBox.cs
  4. 36
      src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs
  5. 12
      src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
  6. 74
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  7. 2
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  8. 2
      src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
  9. 27
      src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs

18
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);
}

4
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);
}

135
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
/// </summary>
public void ClearSelection()
{
SelectionStart = SelectionEnd = CaretIndex;
CaretIndex = SelectionStart;
}
/// <summary>
@ -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] == ' ')
{

36
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;
}
}
}

12
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;
}

74
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<ShapeableTextCharacters>(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<ShapeableTextCharacters>(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<char>(text.Buffer, start, length, bufferOffset);
index++;
shapedTextCharacters.Add(shapedCharacters);
currentRun = nextRun;
continue;
}
break;
}
shapedTextCharacters.AddRange(ShapeTogether(groupedRuns, text));
}
return shapedTextCharacters;
}
private static IReadOnlyList<ShapedTextCharacters> ShapeTogether(
IReadOnlyList<ShapeableTextCharacters> textRuns, ReadOnlySlice<char> text)
{
var shapedRuns = new List<ShapedTextCharacters>(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;
}
/// <summary>
/// Coalesces ranges of the same bidi level to form <see cref="ShapeableTextCharacters"/>
/// </summary>

2
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;

2
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)
{

27
src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs

@ -13,7 +13,7 @@ namespace Avalonia.Utilities
[DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))]
public readonly struct ReadOnlySlice<T> : IReadOnlyList<T> where T : struct
{
private readonly int _offset;
private readonly int _bufferOffset;
/// <summary>
/// Gets an empty <see cref="ReadOnlySlice{T}"/>
@ -24,7 +24,7 @@ namespace Avalonia.Utilities
public ReadOnlySlice(ReadOnlyMemory<T> buffer) : this(buffer, 0, buffer.Length) { }
public ReadOnlySlice(ReadOnlyMemory<T> buffer, int start, int length, int offset = 0)
public ReadOnlySlice(ReadOnlyMemory<T> 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;
}
/// <summary>
@ -74,12 +74,17 @@ namespace Avalonia.Utilities
public bool IsEmpty => Length == 0;
/// <summary>
/// The underlying span.
/// Get the underlying span.
/// </summary>
public ReadOnlySpan<T> Span => _buffer.Span.Slice(_offset, Length);
public ReadOnlySpan<T> Span => _buffer.Span.Slice(_bufferOffset, Length);
/// <summary>
/// The underlying buffer.
/// Get the buffer offset.
/// </summary>
public int BufferOffset => _bufferOffset;
/// <summary>
/// Get the underlying buffer.
/// </summary>
public ReadOnlyMemory<T> 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<T>(_buffer, start, length, _offset);
return new ReadOnlySlice<T>(_buffer, start, length, _bufferOffset);
}
/// <summary>
@ -154,7 +159,7 @@ namespace Avalonia.Utilities
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ReadOnlySlice<T>(_buffer, Start, length, _offset);
return new ReadOnlySlice<T>(_buffer, Start, length, _bufferOffset);
}
/// <summary>
@ -174,7 +179,7 @@ namespace Avalonia.Utilities
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ReadOnlySlice<T>(_buffer, Start + length, Length - length, _offset + length);
return new ReadOnlySlice<T>(_buffer, Start + length, Length - length, _bufferOffset + length);
}
/// <summary>

Loading…
Cancel
Save