diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index d94d36ef1d..8595175192 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -1,9 +1,7 @@ using System; using Avalonia.Controls.Presenters; using Avalonia.Input.TextInput; -using Avalonia.Media.TextFormatting; using Avalonia.Reactive; -using Avalonia.Utilities; namespace Avalonia.Controls { @@ -13,6 +11,7 @@ namespace Avalonia.Controls private TextPresenter? _presenter; private bool _selectionChanged; private bool _isInChange; + private EventHandler? _caretBoundsChangedHandler; public override Visual TextViewVisual => _presenter!; @@ -20,28 +19,22 @@ namespace Avalonia.Controls { get { - if (_presenter is null || _parent is null) + if (_parent is null) { return ""; } - if (_parent.CaretIndex != _presenter.CaretIndex) + if (_presenter is not null && _parent.CaretIndex != _presenter.CaretIndex) { _presenter.SetCurrentValue(TextPresenter.CaretIndexProperty, _parent.CaretIndex); } - if (_parent.Text != _presenter.Text) + if (_presenter is not null && _parent.Text != _presenter.Text) { _presenter.SetCurrentValue(TextPresenter.TextProperty, _parent.Text); } - var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false); - - var textLine = _presenter.TextLayout.TextLines[lineIndex]; - - var lineText = GetTextLineText(textLine); - - return lineText; + return _parent.Text ?? string.Empty; } } @@ -69,41 +62,22 @@ namespace Avalonia.Controls { get { - if (_presenter is null || _parent is null) + if (_parent is null) { return default; } - var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_parent.CaretIndex, false); - - var textLine = _presenter.TextLayout.TextLines[lineIndex]; - - var lineStart = textLine.FirstTextSourceIndex; - - var selectionStart = Math.Max(0, _parent.SelectionStart - lineStart); - - var selectionEnd = Math.Max(0, _parent.SelectionEnd - lineStart); - - return new TextSelection(selectionStart, selectionEnd); + return new TextSelection(_parent.SelectionStart, _parent.SelectionEnd); } set { - if (_parent is null || _presenter is null) + if (_parent is null) { return; } - var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_parent.CaretIndex, false); - - var textLine = _presenter.TextLayout.TextLines[lineIndex]; - - var lineStart = textLine.FirstTextSourceIndex; - - var selectionStart = lineStart + value.Start; - var selectionEnd = lineStart + value.End; - - _parent.SelectionStart = selectionStart; - _parent.SelectionEnd = selectionEnd; + _parent.SelectionStart = value.Start; + _parent.SelectionEnd = value.End; RaiseSelectionChanged(); } @@ -136,7 +110,10 @@ namespace Avalonia.Controls oldPresenter.CurrentImClient = null; oldPresenter.ClearValue(TextPresenter.PreeditTextProperty); - oldPresenter.CaretBoundsChanged -= (s, e) => RaiseCursorRectangleChanged(); + if (_caretBoundsChangedHandler is not null) + { + oldPresenter.CaretBoundsChanged -= _caretBoundsChangedHandler; + } } _presenter = presenter; @@ -145,7 +122,8 @@ namespace Avalonia.Controls { _presenter.CurrentImClient = this; - _presenter.CaretBoundsChanged += (s, e) => RaiseCursorRectangleChanged(); + _caretBoundsChangedHandler ??= OnPresenterCaretBoundsChanged; + _presenter.CaretBoundsChanged += _caretBoundsChangedHandler; } RaiseTextViewVisualChanged(); @@ -153,6 +131,11 @@ namespace Avalonia.Controls RaiseCursorRectangleChanged(); } + private void OnPresenterCaretBoundsChanged(object? sender, EventArgs e) + { + RaiseCursorRectangleChanged(); + } + private void OnParentTapped(object? sender, Input.TappedEventArgs e) { RaiseInputPaneActivationRequested(); @@ -171,30 +154,6 @@ namespace Avalonia.Controls _presenter.SetCurrentValue(TextPresenter.PreeditTextCursorPositionProperty, cursorPos); } - private static string GetTextLineText(TextLine textLine) - { - if (textLine.Length == 0) - { - return string.Empty; - } - - var builder = StringBuilderCache.Acquire(textLine.Length); - - foreach (var run in textLine.TextRuns) - { - if (run.Length > 0) - { - builder.Append(run.Text.Span); - } - } - - var lineText = builder.ToString(); - - StringBuilderCache.Release(builder); - - return lineText; - } - public override void ExecuteContextMenuAction(ContextMenuAction action) { base.ExecuteContextMenuAction(action); diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 46af86b98d..1d7570c2a1 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -2196,6 +2196,46 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(string.Empty, client.SurroundingText); } + [Fact] + public void InputMethodClient_SurroundingText_Uses_Full_Document_For_Multiline_Text() + { + using var _ = UnitTestApplication.Start(Services); + + var textBox = new TextBox + { + Template = CreateTemplate(), + Text = "one\ntwo", + CaretIndex = 5 + }; + textBox.ApplyTemplate(); + + var client = GetInputMethodClient(textBox); + + Assert.Equal("one\ntwo", client.SurroundingText); + Assert.Equal(new TextSelection(5, 5), client.Selection); + } + + [Fact] + public void InputMethodClient_Selection_Setter_Uses_Document_Offsets_For_Multiline_Text() + { + using var _ = UnitTestApplication.Start(Services); + + var textBox = new TextBox + { + Template = CreateTemplate(), + Text = "one\ntwo", + CaretIndex = 5 + }; + textBox.ApplyTemplate(); + + var client = GetInputMethodClient(textBox); + client.Selection = new TextSelection(0, 3); + + Assert.Equal(0, textBox.SelectionStart); + Assert.Equal(3, textBox.SelectionEnd); + Assert.Equal("one", textBox.SelectedText); + } + [Fact] public void Backspace_Should_Delete_Last_Character_In_Line_And_Keep_Caret_On_Same_Line() { @@ -2364,6 +2404,18 @@ namespace Avalonia.Controls.UnitTests fontManagerImpl: new TestFontManager(), assetLoader: new StandardAssetLoader()); + private static TextInputMethodClient GetInputMethodClient(TextBox textBox) + { + var eventArgs = new TextInputMethodClientRequestedEventArgs + { + RoutedEvent = InputElement.TextInputMethodClientRequestedEvent + }; + textBox.RaiseEvent(eventArgs); + + Assert.NotNull(eventArgs.Client); + return eventArgs.Client; + } + internal static IControlTemplate CreateTemplate() { return new FuncControlTemplate((control, scope) =>