Browse Source

Fix Android multiline TextBox IME offsets (#21680)

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
pull/21852/head
AlexSCcon 2 months ago
committed by GitHub
parent
commit
561b9a375c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 83
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  2. 52
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

83
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);

52
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<TextBox>((control, scope) =>

Loading…
Cancel
Save