Skip to content

Commit 561b9a3

Browse files
AlexSCconMrJul
andauthored
Fix Android multiline TextBox IME offsets (#21680)
Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
1 parent 4fc9848 commit 561b9a3

2 files changed

Lines changed: 73 additions & 62 deletions

File tree

src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

Lines changed: 21 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
using System;
22
using Avalonia.Controls.Presenters;
33
using Avalonia.Input.TextInput;
4-
using Avalonia.Media.TextFormatting;
54
using Avalonia.Reactive;
6-
using Avalonia.Utilities;
75

86
namespace Avalonia.Controls
97
{
@@ -13,35 +11,30 @@ internal class TextBoxTextInputMethodClient : TextInputMethodClient
1311
private TextPresenter? _presenter;
1412
private bool _selectionChanged;
1513
private bool _isInChange;
14+
private EventHandler? _caretBoundsChangedHandler;
1615

1716
public override Visual TextViewVisual => _presenter!;
1817

1918
public override string SurroundingText
2019
{
2120
get
2221
{
23-
if (_presenter is null || _parent is null)
22+
if (_parent is null)
2423
{
2524
return "";
2625
}
2726

28-
if (_parent.CaretIndex != _presenter.CaretIndex)
27+
if (_presenter is not null && _parent.CaretIndex != _presenter.CaretIndex)
2928
{
3029
_presenter.SetCurrentValue(TextPresenter.CaretIndexProperty, _parent.CaretIndex);
3130
}
3231

33-
if (_parent.Text != _presenter.Text)
32+
if (_presenter is not null && _parent.Text != _presenter.Text)
3433
{
3534
_presenter.SetCurrentValue(TextPresenter.TextProperty, _parent.Text);
3635
}
3736

38-
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false);
39-
40-
var textLine = _presenter.TextLayout.TextLines[lineIndex];
41-
42-
var lineText = GetTextLineText(textLine);
43-
44-
return lineText;
37+
return _parent.Text ?? string.Empty;
4538
}
4639
}
4740

@@ -69,41 +62,22 @@ public override TextSelection Selection
6962
{
7063
get
7164
{
72-
if (_presenter is null || _parent is null)
65+
if (_parent is null)
7366
{
7467
return default;
7568
}
7669

77-
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_parent.CaretIndex, false);
78-
79-
var textLine = _presenter.TextLayout.TextLines[lineIndex];
80-
81-
var lineStart = textLine.FirstTextSourceIndex;
82-
83-
var selectionStart = Math.Max(0, _parent.SelectionStart - lineStart);
84-
85-
var selectionEnd = Math.Max(0, _parent.SelectionEnd - lineStart);
86-
87-
return new TextSelection(selectionStart, selectionEnd);
70+
return new TextSelection(_parent.SelectionStart, _parent.SelectionEnd);
8871
}
8972
set
9073
{
91-
if (_parent is null || _presenter is null)
74+
if (_parent is null)
9275
{
9376
return;
9477
}
9578

96-
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_parent.CaretIndex, false);
97-
98-
var textLine = _presenter.TextLayout.TextLines[lineIndex];
99-
100-
var lineStart = textLine.FirstTextSourceIndex;
101-
102-
var selectionStart = lineStart + value.Start;
103-
var selectionEnd = lineStart + value.End;
104-
105-
_parent.SelectionStart = selectionStart;
106-
_parent.SelectionEnd = selectionEnd;
79+
_parent.SelectionStart = value.Start;
80+
_parent.SelectionEnd = value.End;
10781

10882
RaiseSelectionChanged();
10983
}
@@ -136,7 +110,10 @@ public void SetPresenter(TextPresenter? presenter, TextBox? parent)
136110
oldPresenter.CurrentImClient = null;
137111
oldPresenter.ClearValue(TextPresenter.PreeditTextProperty);
138112

139-
oldPresenter.CaretBoundsChanged -= (s, e) => RaiseCursorRectangleChanged();
113+
if (_caretBoundsChangedHandler is not null)
114+
{
115+
oldPresenter.CaretBoundsChanged -= _caretBoundsChangedHandler;
116+
}
140117
}
141118

142119
_presenter = presenter;
@@ -145,14 +122,20 @@ public void SetPresenter(TextPresenter? presenter, TextBox? parent)
145122
{
146123

147124
_presenter.CurrentImClient = this;
148-
_presenter.CaretBoundsChanged += (s, e) => RaiseCursorRectangleChanged();
125+
_caretBoundsChangedHandler ??= OnPresenterCaretBoundsChanged;
126+
_presenter.CaretBoundsChanged += _caretBoundsChangedHandler;
149127
}
150128

151129
RaiseTextViewVisualChanged();
152130

153131
RaiseCursorRectangleChanged();
154132
}
155133

134+
private void OnPresenterCaretBoundsChanged(object? sender, EventArgs e)
135+
{
136+
RaiseCursorRectangleChanged();
137+
}
138+
156139
private void OnParentTapped(object? sender, Input.TappedEventArgs e)
157140
{
158141
RaiseInputPaneActivationRequested();
@@ -171,30 +154,6 @@ public override void SetPreeditText(string? preeditText, int? cursorPos)
171154
_presenter.SetCurrentValue(TextPresenter.PreeditTextCursorPositionProperty, cursorPos);
172155
}
173156

174-
private static string GetTextLineText(TextLine textLine)
175-
{
176-
if (textLine.Length == 0)
177-
{
178-
return string.Empty;
179-
}
180-
181-
var builder = StringBuilderCache.Acquire(textLine.Length);
182-
183-
foreach (var run in textLine.TextRuns)
184-
{
185-
if (run.Length > 0)
186-
{
187-
builder.Append(run.Text.Span);
188-
}
189-
}
190-
191-
var lineText = builder.ToString();
192-
193-
StringBuilderCache.Release(builder);
194-
195-
return lineText;
196-
}
197-
198157
public override void ExecuteContextMenuAction(ContextMenuAction action)
199158
{
200159
base.ExecuteContextMenuAction(action);

tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2196,6 +2196,46 @@ public void InputMethodClient_SurroundingText_Returns_Empty_For_Empty_Line()
21962196
Assert.Equal(string.Empty, client.SurroundingText);
21972197
}
21982198

2199+
[Fact]
2200+
public void InputMethodClient_SurroundingText_Uses_Full_Document_For_Multiline_Text()
2201+
{
2202+
using var _ = UnitTestApplication.Start(Services);
2203+
2204+
var textBox = new TextBox
2205+
{
2206+
Template = CreateTemplate(),
2207+
Text = "one\ntwo",
2208+
CaretIndex = 5
2209+
};
2210+
textBox.ApplyTemplate();
2211+
2212+
var client = GetInputMethodClient(textBox);
2213+
2214+
Assert.Equal("one\ntwo", client.SurroundingText);
2215+
Assert.Equal(new TextSelection(5, 5), client.Selection);
2216+
}
2217+
2218+
[Fact]
2219+
public void InputMethodClient_Selection_Setter_Uses_Document_Offsets_For_Multiline_Text()
2220+
{
2221+
using var _ = UnitTestApplication.Start(Services);
2222+
2223+
var textBox = new TextBox
2224+
{
2225+
Template = CreateTemplate(),
2226+
Text = "one\ntwo",
2227+
CaretIndex = 5
2228+
};
2229+
textBox.ApplyTemplate();
2230+
2231+
var client = GetInputMethodClient(textBox);
2232+
client.Selection = new TextSelection(0, 3);
2233+
2234+
Assert.Equal(0, textBox.SelectionStart);
2235+
Assert.Equal(3, textBox.SelectionEnd);
2236+
Assert.Equal("one", textBox.SelectedText);
2237+
}
2238+
21992239
[Fact]
22002240
public void Backspace_Should_Delete_Last_Character_In_Line_And_Keep_Caret_On_Same_Line()
22012241
{
@@ -2364,6 +2404,18 @@ public void PlaceholderForeground_Can_Be_Set_To_Null()
23642404
fontManagerImpl: new TestFontManager(),
23652405
assetLoader: new StandardAssetLoader());
23662406

2407+
private static TextInputMethodClient GetInputMethodClient(TextBox textBox)
2408+
{
2409+
var eventArgs = new TextInputMethodClientRequestedEventArgs
2410+
{
2411+
RoutedEvent = InputElement.TextInputMethodClientRequestedEvent
2412+
};
2413+
textBox.RaiseEvent(eventArgs);
2414+
2415+
Assert.NotNull(eventArgs.Client);
2416+
return eventArgs.Client;
2417+
}
2418+
23672419
internal static IControlTemplate CreateTemplate()
23682420
{
23692421
return new FuncControlTemplate<TextBox>((control, scope) =>

0 commit comments

Comments
 (0)