diff --git a/src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs b/src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs index 2e8e145ef8..aa6bd5740b 100644 --- a/src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs @@ -126,7 +126,7 @@ namespace Avalonia.Android.Platform.Input _inputConnection.IsInUpdate = true; - var selection = Client.Selection; + var selection = Client.ActualSelection; var composition = _inputConnection.EditBuffer.HasComposition ? _inputConnection.EditBuffer.Composition!.Value : new TextSelection(-1,-1); diff --git a/src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs b/src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs index 39e1574901..3c7e13f8ef 100644 --- a/src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs +++ b/src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs @@ -284,15 +284,12 @@ namespace Avalonia.Android.Platform.Input public ICharSequence? GetTextAfterCursorFormatted(int n, [GeneratedEnum] GetTextFlags flags) { - var end = Math.Min(_editBuffer.Selection.End, _editBuffer.Text.Length); - return SafeSubstring(_editBuffer.Text, end, Math.Min(n, _editBuffer.Text.Length - end)); + return _editBuffer.GetTextAfterCursor(n); } public ICharSequence? GetTextBeforeCursorFormatted(int n, [GeneratedEnum] GetTextFlags flags) { - var start = Math.Max(0, _editBuffer.Selection.Start - n); - var length = _editBuffer.Selection.Start - start; - return SafeSubstring(_editBuffer.Text, start, length); + return _editBuffer.GetTextBeforeCursor(n); } public bool PerformPrivateCommand(string? action, Bundle? data) diff --git a/src/Android/Avalonia.Android/Platform/Input/EditCommand.cs b/src/Android/Avalonia.Android/Platform/Input/EditCommand.cs index c110807dd9..5992a22dbb 100644 --- a/src/Android/Avalonia.Android/Platform/Input/EditCommand.cs +++ b/src/Android/Avalonia.Android/Platform/Input/EditCommand.cs @@ -21,9 +21,7 @@ namespace Avalonia.Android.Platform.Input public override void Apply(TextEditBuffer buffer) { - var start = Math.Clamp(_start, 0, buffer.Text.Length); - var end = Math.Clamp(_end, 0, buffer.Text.Length); - buffer.Selection = new TextSelection(start, end); + buffer.Selection = new TextSelection(_start, _end); } } @@ -57,11 +55,13 @@ namespace Avalonia.Android.Platform.Input public override void Apply(TextEditBuffer buffer) { - var end = Math.Min(buffer.Text.Length, buffer.Selection.End + _after); - var endCount = end - buffer.Selection.End; - var start = Math.Max(0, buffer.Selection.Start - _before); - buffer.Remove(buffer.Selection.End, endCount); - buffer.Remove(start, buffer.Selection.Start - start); + var length = buffer.Text.Length; + var selection = buffer.Selection; + var end = Math.Min(length, selection.End + _after); + var endCount = end - selection.End; + var start = Math.Max(0, selection.Start - _before); + buffer.Remove(selection.End, endCount); + buffer.Remove(start, selection.Start - start); buffer.Selection = new TextSelection(start, start); } } @@ -81,13 +81,15 @@ namespace Avalonia.Android.Platform.Input { var beforeLengthInChar = 0; + var selection = buffer.Selection; + var text = buffer.Text; for (int i = 0; i < _before; i++) { beforeLengthInChar++; - if (buffer.Selection.Start > beforeLengthInChar) + if (selection.Start > beforeLengthInChar) { - var lead = buffer.Text[buffer.Selection.Start - beforeLengthInChar - 1]; - var trail = buffer.Text[buffer.Selection.Start - beforeLengthInChar]; + var lead = text[selection.Start - beforeLengthInChar - 1]; + var trail = text[selection.Start - beforeLengthInChar]; if (char.IsSurrogatePair(lead, trail)) { @@ -95,7 +97,7 @@ namespace Avalonia.Android.Platform.Input } } - if (beforeLengthInChar == buffer.Selection.Start) + if (beforeLengthInChar == selection.Start) break; } @@ -103,10 +105,10 @@ namespace Avalonia.Android.Platform.Input for (int i = 0; i < _after; i++) { afterLengthInChar++; - if (buffer.Selection.End > afterLengthInChar) + if (selection.End > afterLengthInChar) { - var lead = buffer.Text[buffer.Selection.End + afterLengthInChar - 1]; - var trail = buffer.Text[buffer.Selection.End + afterLengthInChar]; + var lead = text[selection.End + afterLengthInChar - 1]; + var trail = text[selection.End + afterLengthInChar]; if (char.IsSurrogatePair(lead, trail)) { @@ -114,12 +116,12 @@ namespace Avalonia.Android.Platform.Input } } - if (buffer.Selection.End + afterLengthInChar == buffer.Text.Length) + if (selection.End + afterLengthInChar == text.Length) break; } - var start = buffer.Selection.Start - beforeLengthInChar; - buffer.Remove(buffer.Selection.End, afterLengthInChar); + var start = selection.Start - beforeLengthInChar; + buffer.Remove(selection.End, afterLengthInChar); buffer.Remove(start, beforeLengthInChar); buffer.Selection = new TextSelection(start, start); } @@ -139,7 +141,8 @@ namespace Avalonia.Android.Platform.Input public override void Apply(TextEditBuffer buffer) { buffer.ComposingText = _text; - var newCursor = _newCursorPosition > 0 ? buffer.Selection.Start + _newCursorPosition - 1 : buffer.Selection.Start + _newCursorPosition; + var selection = buffer.Selection; + var newCursor = _newCursorPosition > 0 ? selection.Start + _newCursorPosition - 1 : selection.Start + _newCursorPosition; buffer.Selection = new TextSelection(newCursor, newCursor); } } diff --git a/src/Android/Avalonia.Android/Platform/Input/TextEditBuffer.cs b/src/Android/Avalonia.Android/Platform/Input/TextEditBuffer.cs index e040687d6d..1490fdffd3 100644 --- a/src/Android/Avalonia.Android/Platform/Input/TextEditBuffer.cs +++ b/src/Android/Avalonia.Android/Platform/Input/TextEditBuffer.cs @@ -1,5 +1,4 @@ using System; -using Android.Text; using Android.Views; using Android.Views.InputMethods; using Avalonia.Android.Platform.SkiaPlatform; @@ -25,14 +24,14 @@ namespace Avalonia.Android.Platform.Input { get { - var selection = _textInputMethod.Client?.Selection ?? default; + var selection = _textInputMethod.Client?.ActualSelection ?? default; return new TextSelection(Math.Min(selection.Start, selection.End), Math.Max(selection.Start, selection.End)); } set { if (_textInputMethod.Client is { } client) - client.Selection = value; + client.ActualSelection = value; } } @@ -56,14 +55,14 @@ namespace Avalonia.Android.Platform.Input { get { - if(_textInputMethod.Client is not { } client || Selection.Start < 0 || Selection.End >= client.SurroundingText.Length) + if(_textInputMethod.Client is not { } client || Selection.Start < 0 || Selection.End >= client.Text.Length) { - return ""; + return string.Empty; } var selection = Selection; - return client.SurroundingText.Substring(selection.Start, selection.End - selection.Start); + return Text.Substring(selection.Start, selection.End - selection.Start); } } @@ -74,36 +73,46 @@ namespace Avalonia.Android.Platform.Input if (HasComposition) { var start = Composition!.Value.Start; - Replace(Composition!.Value.Start, Composition!.Value.End, value ?? ""); + Replace(Composition!.Value.Start, Composition!.Value.End, value ?? string.Empty); Composition = new TextSelection(start, start + (value?.Length ?? 0)); } else { var selection = Selection; - Replace(selection.Start, selection.End, value ?? ""); + Replace(selection.Start, selection.End, value ?? string.Empty); Composition = new TextSelection(selection.Start, selection.Start + (value?.Length ?? 0)); } } } - public string Text => _textInputMethod.Client?.SurroundingText ?? ""; + public string Text => _textInputMethod.Client?.Text ?? string.Empty; - public ExtractedText? ExtractedText => new ExtractedText + public ExtractedText? ExtractedText { - Flags = Text.Contains('\n') ? 0 : ExtractedTextFlags.SingleLine, - PartialStartOffset = -1, - PartialEndOffset = Text.Length, - SelectionStart = Selection.Start, - SelectionEnd = Selection.End, - StartOffset = 0, - Text = new Java.Lang.String(Text) - }; + get + { + var text = Text; + return new ExtractedText + { + Flags = text.Contains('\n') ? 0 : ExtractedTextFlags.SingleLine, + PartialStartOffset = -1, + PartialEndOffset = text.Length, + SelectionStart = Selection.Start, + SelectionEnd = Selection.End, + StartOffset = 0, + Text = new Java.Lang.String(text) + }; + } + } + + internal Java.Lang.ICharSequence? GetTextBeforeCursor(int n) => new Java.Lang.String(_textInputMethod.Client?.GetTextBeforeCaret(n) ?? string.Empty); + internal Java.Lang.ICharSequence? GetTextAfterCursor(int n) => new Java.Lang.String(_textInputMethod.Client?.GetTextAfterCaret(n) ?? string.Empty); internal void Remove(int index, int length) { if (_textInputMethod.Client is { } client) { - client.Selection = new TextSelection(index, index + length); + client.ActualSelection = new TextSelection(index, index + length); if (length > 0) _textInputMethod?.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); } @@ -117,12 +126,12 @@ namespace Avalonia.Android.Platform.Input var realEnd = Math.Max(start, end); if (realEnd > realStart) { - client.Selection = new TextSelection(realStart, realEnd); + client.ActualSelection = new TextSelection(realStart, realEnd); _textInputMethod?.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); } _topLevel.TextInput(text); var index = realStart + text.Length; - client.Selection = new TextSelection(index, index); + client.ActualSelection = new TextSelection(index, index); Composition = null; } } diff --git a/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs index 7f9870315b..5a07e49ba7 100644 --- a/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs +++ b/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs @@ -54,6 +54,8 @@ namespace Avalonia.Input.TextInput /// public abstract string SurroundingText { get; } + internal virtual string Text => SurroundingText; + /// /// Gets the cursor rectangle relative to the TextViewVisual /// @@ -119,6 +121,11 @@ namespace Avalonia.Input.TextInput { ResetRequested?.Invoke(this, EventArgs.Empty); } + + internal virtual string GetTextBeforeCaret(int length) => string.Empty; + internal virtual string GetTextAfterCaret(int length) => string.Empty; + + internal virtual TextSelection ActualSelection { get => Selection; set => Selection = value; } } public record struct TextSelection(int Start, int End); diff --git a/src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs b/src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs index 4248ea65a1..a592cf77cf 100644 --- a/src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs +++ b/src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs @@ -177,22 +177,30 @@ namespace Avalonia.Controls.Primitives var selectionEnd = _textBox.SelectionEnd; var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; - var rects = new List(_presenter.TextLayout.HitTestTextRange(start, length)); - if (rects.Count > 0) + try { - var first = rects[0]; - var last = rects[rects.Count -1]; + var rects = new List(_presenter.TextLayout.HitTestTextRange(start, length)); - if (handle.SelectionHandleType == SelectionHandleType.Start) - handle?.SetTopLeft(ToLayer(first.BottomLeft)); - else - handle?.SetTopLeft(ToLayer(last.BottomRight)); + if (rects.Count > 0) + { + var first = rects[0]; + var last = rects[rects.Count - 1]; + + if (handle.SelectionHandleType == SelectionHandleType.Start) + handle?.SetTopLeft(ToLayer(first.BottomLeft)); + else + handle?.SetTopLeft(ToLayer(last.BottomRight)); + + if (otherHandle.SelectionHandleType == SelectionHandleType.Start) + otherHandle?.SetTopLeft(ToLayer(first.BottomLeft)); + else + otherHandle?.SetTopLeft(ToLayer(last.BottomRight)); + } + } + catch(InvalidOperationException) + { - if (otherHandle.SelectionHandleType == SelectionHandleType.Start) - otherHandle?.SetTopLeft(ToLayer(first.BottomLeft)); - else - otherHandle?.SetTopLeft(ToLayer(last.BottomRight)); } _presenter?.MoveCaretToTextPosition(position); @@ -241,29 +249,36 @@ namespace Avalonia.Controls.Primitives var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; - var rects = new List(_presenter.TextLayout.HitTestTextRange(start, length)); - - if (rects.Count > 0) + try { - var first = rects[0]; - var last = rects[rects.Count - 1]; + var rects = new List(_presenter.TextLayout.HitTestTextRange(start, length)); - if (!_startHandle.IsDragging) + if (rects.Count > 0) { - _startHandle.SetTopLeft(ToLayer(first.BottomLeft)); - _startHandle.SelectionHandleType = selectionStart < selectionEnd ? - SelectionHandleType.Start : - SelectionHandleType.End; - } + var first = rects[0]; + var last = rects[rects.Count - 1]; - if (!_endHandle.IsDragging) - { - _endHandle.SetTopLeft(ToLayer(last.BottomRight)); - _endHandle.SelectionHandleType = selectionStart > selectionEnd ? - SelectionHandleType.Start : - SelectionHandleType.End; + if (!_startHandle.IsDragging) + { + _startHandle.SetTopLeft(ToLayer(first.BottomLeft)); + _startHandle.SelectionHandleType = selectionStart < selectionEnd ? + SelectionHandleType.Start : + SelectionHandleType.End; + } + + if (!_endHandle.IsDragging) + { + _endHandle.SetTopLeft(ToLayer(last.BottomRight)); + _endHandle.SelectionHandleType = selectionStart > selectionEnd ? + SelectionHandleType.Start : + SelectionHandleType.End; + } } } + catch (InvalidOperationException) + { + + } } } diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 12e8e97640..d332ee2b30 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -1,4 +1,5 @@ using System; +using System.Text; using Avalonia.Controls.Presenters; using Avalonia.Input.TextInput; using Avalonia.Media.TextFormatting; @@ -45,6 +46,101 @@ namespace Avalonia.Controls } } + internal override string Text => _presenter?.Text ?? string.Empty; + + internal override string GetTextBeforeCaret(int length) + { + if (_presenter is null || _parent is null) + { + return ""; + } + var selectionStart = _presenter.SelectionStart; + + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(selectionStart, false); + + var textLine = _presenter.TextLayout.TextLines[lineIndex]; + + var offset = selectionStart - textLine.FirstTextSourceIndex; + + var currentLineLength = Math.Min(offset, length); + var start = Math.Max(offset - currentLineLength, 0); + + var lineText = GetTextLineText(textLine); + var text = lineText.Substring(start, currentLineLength); + + var newText = text; + + length -= currentLineLength; + + while (length > 0) + { + lineIndex--; + if (lineIndex >= 0) + { + textLine = _presenter.TextLayout.TextLines[lineIndex]; + currentLineLength = Math.Min(textLine.Length, length); + + lineText = GetTextLineText(textLine); + text = lineText.Substring(textLine.Length - currentLineLength, currentLineLength); + + newText = text + newText; + + length -= currentLineLength; + } + else + break; + } + + return newText; + } + + internal override string GetTextAfterCaret(int length) + { + if (_presenter is null || _parent is null) + { + return ""; + } + + var selectionEnd = _presenter.SelectionStart; + + var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(selectionEnd, false); + + var textLine = _presenter.TextLayout.TextLines[lineIndex]; + var lastIndex = textLine.FirstTextSourceIndex + textLine.Length; + + var currentLineLength = Math.Min(lastIndex - selectionEnd, length); + var start = Math.Max(selectionEnd - textLine.FirstTextSourceIndex, 0); + + var builder = new StringBuilder(); + + var lineText = GetTextLineText(textLine); + + builder.Append(lineText.Substring(start, currentLineLength)); + + length -= currentLineLength; + + while (length > 0) + { + lineIndex++; + if (lineIndex < _presenter.TextLayout.TextLines.Count) + { + textLine = _presenter.TextLayout.TextLines[lineIndex]; + currentLineLength = Math.Min(textLine.Length, length); + + lineText = GetTextLineText(textLine); + var text = lineText.Substring(0, currentLineLength); + + builder.Append(text); + + length -= currentLineLength; + } + else + break; + } + + return builder.ToString(); + } + public override Rect CursorRectangle { get @@ -109,6 +205,30 @@ namespace Avalonia.Controls } } + internal override TextSelection ActualSelection + { + get + { + if (_presenter is null || _parent is null) + { + return default; + } + + return new TextSelection(_presenter.SelectionStart, _presenter.SelectionEnd); + } + + set + { + if (_parent is not null) + { + _parent.SelectionStart = value.Start; + _parent.SelectionEnd = value.End; + + RaiseSelectionChanged(); + } + } + } + public override bool SupportsPreedit => true; public override bool SupportsSurroundingText => true;