Browse Source

Rework ITextInputMethodClient

pull/11848/head
Benedikt Stebner 3 years ago
parent
commit
be9a26cbb1
  1. 15
      samples/MobileSandbox.Android/MainActivity.cs
  2. 4
      samples/MobileSandbox.Android/MobileSandbox.Android.csproj
  3. 70
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  4. 3
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  5. 127
      src/Android/Avalonia.Android/InputEditable.cs
  6. 172
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  7. 23
      src/Avalonia.Base/Input/TextInput/ITextEditable.cs
  8. 66
      src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
  9. 2
      src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs
  10. 4
      src/Avalonia.Base/Input/TextInput/InputMethodManager.cs
  11. 136
      src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs
  12. 2
      src/Avalonia.Base/Input/TextInput/TextInputMethodClientRequestedEventArgs.cs
  13. 94
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  14. 4
      src/Avalonia.Controls/TextBox.cs
  15. 292
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  16. 6
      src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
  17. 18
      src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs
  18. 6
      src/Avalonia.X11/X11Window.Xim.cs
  19. 18
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  20. 4
      src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs
  21. 2
      src/iOS/Avalonia.iOS/AvaloniaView.Text.cs
  22. 2
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  23. 36
      src/iOS/Avalonia.iOS/TextInputResponder.cs

15
samples/MobileSandbox.Android/MainActivity.cs

@ -1,5 +1,7 @@
using Android.App;
using System;
using Android.App;
using Android.Content.PM;
using Avalonia;
using Avalonia.Android;
namespace MobileSandbox.Android
@ -7,5 +9,16 @@ namespace MobileSandbox.Android
[Activity(Label = "MobileSandbox.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)]
public class MainActivity : AvaloniaMainActivity<App>
{
protected override void OnCreate(Bundle savedInstanceState)
{
AppDomain.CurrentDomain.UnhandledException += CurrentDomainUnhandledException;
base.OnCreate(savedInstanceState);
}
private void CurrentDomainUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
System.Diagnostics.Debug.WriteLine(e.ToString());
}
}
}

4
samples/MobileSandbox.Android/MobileSandbox.Android.csproj

@ -38,6 +38,10 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<AndroidEnvironment Condition="'$(IsEmulator)'=='True'" Include="environment.emulator.txt" />
<AndroidEnvironment Condition="'$(IsEmulator)'!='True'" Include="environment.device.txt" />

70
src/Android/Avalonia.Android/AndroidInputMethod.cs

@ -1,10 +1,10 @@
using System;
using Android.Content;
using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls.Presenters;
using Avalonia.Input.TextInput;
namespace Avalonia.Android
@ -13,7 +13,7 @@ namespace Avalonia.Android
{
public View View { get; }
public ITextInputMethodClient Client { get; }
public TextInputMethodClient Client { get; }
public bool IsActive { get; }
@ -36,7 +36,7 @@ namespace Avalonia.Android
{
private readonly TView _host;
private readonly InputMethodManager _imm;
private ITextInputMethodClient _client;
private TextInputMethodClient _client;
private AvaloniaInputConnection _inputConnection;
public AndroidInputMethod(TView host)
@ -56,7 +56,7 @@ namespace Avalonia.Android
public bool IsActive => Client != null;
public ITextInputMethodClient Client => _client;
public TextInputMethodClient Client => _client;
public InputMethodManager IMM => _imm;
@ -65,7 +65,7 @@ namespace Avalonia.Android
}
public void SetClient(ITextInputMethodClient client)
public void SetClient(TextInputMethodClient client)
{
_client = client;
@ -77,9 +77,24 @@ namespace Avalonia.Android
_imm.ShowSoftInput(_host, ShowFlags.Implicit);
var surroundingText = Client.SurroundingText;
var selection = Client.Selection;
_imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset);
_imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End);
var surroundingText = _client.SurroundingText ?? "";
var extractedText = new ExtractedText
{
Text = new Java.Lang.String(surroundingText),
SelectionStart = selection.Start,
SelectionEnd = selection.End,
PartialEndOffset = surroundingText.Length
};
_imm.UpdateExtractedText(_host, _inputConnection?.ExtractedTextToken ?? 0, extractedText);
_client.SurroundingTextChanged += _client_SurroundingTextChanged;
_client.SelectionChanged += _client_SelectionChanged;
}
else
{
@ -87,6 +102,30 @@ namespace Avalonia.Android
}
}
private void _client_SelectionChanged(object sender, EventArgs e)
{
var selection = Client.Selection;
_imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End);
}
private void _client_SurroundingTextChanged(object sender, EventArgs e)
{
var surroundingText = _client.SurroundingText ?? "";
_inputConnection.EditableWrapper.IgnoreChange = true;
_inputConnection.Editable.Replace(0, _inputConnection.Editable.Length(), surroundingText);
_inputConnection.EditableWrapper.IgnoreChange = false;
var selection = Client.Selection;
_imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End);
//Debug.WriteLine($"SurroundingText: {surroundingText}, CaretIndex: {selection.Start}");
}
public void SetCursorRect(Rect rect)
{
@ -134,25 +173,8 @@ namespace Avalonia.Android
outAttrs.ImeOptions |= ImeFlags.NoFullscreen | ImeFlags.NoExtractUi;
_client.TextEditable = _inputConnection.InputEditable;
return _inputConnection;
});
}
}
internal readonly record struct ComposingRegion
{
private readonly int _start = -1;
private readonly int _end = -1;
public ComposingRegion(int start, int end)
{
_start = start;
_end = end;
}
public int Start => _start;
public int End => _end;
}
}

3
src/Android/Avalonia.Android/Avalonia.Android.csproj

@ -7,6 +7,9 @@
<DebugType>portable</DebugType>
<AndroidResgenNamespace>Avalonia.Android.Internal</AndroidResgenNamespace>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" />

127
src/Android/Avalonia.Android/InputEditable.cs

@ -1,127 +0,0 @@
using System;
using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls.Presenters;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Java.Lang;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Android
{
internal class InputEditable : SpannableStringBuilder, ITextEditable
{
private readonly TopLevelImpl _topLevel;
private readonly IAndroidInputMethod _inputMethod;
private readonly AvaloniaInputConnection _avaloniaInputConnection;
private int _currentBatchLevel;
private string _previousText;
private int _previousSelectionStart;
private int _previousSelectionEnd;
public event EventHandler TextChanged;
public event EventHandler SelectionChanged;
public event EventHandler CompositionChanged;
public InputEditable(TopLevelImpl topLevel, IAndroidInputMethod inputMethod, AvaloniaInputConnection avaloniaInputConnection)
{
_topLevel = topLevel;
_inputMethod = inputMethod;
_avaloniaInputConnection = avaloniaInputConnection;
}
public InputEditable(ICharSequence text) : base(text)
{
}
public InputEditable(string text) : base(text)
{
}
public InputEditable(ICharSequence text, int start, int end) : base(text, start, end)
{
}
public InputEditable(string text, int start, int end) : base(text, start, end)
{
}
protected InputEditable(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
{
}
public int SelectionStart
{
get => Selection.GetSelectionStart(this); set
{
var end = SelectionEnd < 0 ? 0 : SelectionEnd;
_avaloniaInputConnection.SetSelection(value, end);
_inputMethod.IMM.UpdateSelection(_topLevel.View, value, end, value, end);
}
}
public int SelectionEnd
{
get => Selection.GetSelectionEnd(this); set
{
var start = SelectionStart < 0 ? 0 : SelectionStart;
_avaloniaInputConnection.SetSelection(start, value);
_inputMethod.IMM.UpdateSelection(_topLevel.View, start, value, start, value);
}
}
public string? Text
{
get => ToString(); set
{
if (Text != value)
{
Clear();
Insert(0, value ?? "");
}
}
}
public int CompositionStart => BaseInputConnection.GetComposingSpanStart(this);
public int CompositionEnd => BaseInputConnection.GetComposingSpanEnd(this);
public void BeginBatchEdit()
{
_currentBatchLevel++;
if (_currentBatchLevel == 1)
{
_previousText = ToString();
_previousSelectionStart = SelectionStart;
_previousSelectionEnd = SelectionEnd;
}
}
public void EndBatchEdit()
{
if (_currentBatchLevel == 1)
{
if(_previousText != Text)
{
TextChanged?.Invoke(this, EventArgs.Empty);
}
if (_previousSelectionStart != SelectionStart || _previousSelectionEnd != SelectionEnd)
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
}
_currentBatchLevel--;
}
public void RaiseCompositionChanged()
{
CompositionChanged?.Invoke(this, EventArgs.Empty);
}
}
}

172
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -11,6 +11,7 @@ using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using AndroidX.AppCompat.App;
using Avalonia.Android.Platform.Input;
using Avalonia.Android.Platform.Specific;
using Avalonia.Android.Platform.Specific.Helpers;
using Avalonia.Android.Platform.Storage;
@ -429,78 +430,124 @@ namespace Avalonia.Android.Platform.SkiaPlatform
activity.Window.Attributes = attr;
}
}
internal void TextInput(string text)
{
if(Input != null)
{
var args = new RawTextInputEventArgs(AndroidKeyboardDevice.Instance, (ulong)DateTime.Now.Ticks, InputRoot, text);
Input(args);
}
}
}
internal class AvaloniaInputConnection : BaseInputConnection
internal class EditableWrapper : SpannableStringBuilder
{
private readonly TopLevelImpl _topLevel;
private readonly IAndroidInputMethod _inputMethod;
private readonly InputEditable _editable;
private readonly AvaloniaInputConnection _inputConnection;
public AvaloniaInputConnection(TopLevelImpl topLevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true)
public EditableWrapper(AvaloniaInputConnection inputConnection)
{
_topLevel = topLevel;
_inputMethod = inputMethod;
_editable = new InputEditable(_topLevel, _inputMethod, this);
_inputConnection = inputConnection;
}
public override IEditable Editable => _editable;
internal InputEditable InputEditable => _editable;
public bool IgnoreChange { get; set; }
public override bool SetComposingRegion(int start, int end)
public override IEditable Replace(int start, int end, ICharSequence tb)
{
var ret = base.SetComposingRegion(start, end);
if (!IgnoreChange && !_inputConnection.IsComposing && start != end)
{
var text = tb.SubSequence(0, tb.Length());
InputEditable.RaiseCompositionChanged();
//System.Diagnostics.Debug.WriteLine($"Replace: start: {start}, end: {end}, text: {text}");
return ret;
_inputConnection.InputMethod.Client.Selection = new TextSelection(start, end);
}
return base.Replace(start, end, tb);
}
public override bool SetComposingText(ICharSequence text, int newCursorPosition)
public override IEditable Replace(int start, int end, ICharSequence tb, int tbstart, int tbend)
{
var composingText = text.ToString();
if (string.IsNullOrEmpty(composingText))
{
return CommitText(text, newCursorPosition);
}
else
if (!IgnoreChange && !_inputConnection.IsComposing && start != end)
{
var ret = base.SetComposingText(text, newCursorPosition);
var text = tb.SubSequence(tbstart, tbend);
InputEditable.RaiseCompositionChanged();
//System.Diagnostics.Debug.WriteLine($"Replace: start: {start}, end: {end}, text: {text}");
return ret;
_inputConnection.InputMethod.Client.Selection = new TextSelection(start, end);
}
return base.Replace(start, end, tb, tbstart, tbend);
}
}
public override bool BeginBatchEdit()
{
_editable.BeginBatchEdit();
internal class AvaloniaInputConnection : BaseInputConnection
{
private readonly TopLevelImpl _toplevel;
private readonly IAndroidInputMethod _inputMethod;
private readonly EditableWrapper _editable;
private string _compositionText;
private bool _commitInProgress;
return base.BeginBatchEdit();
public AvaloniaInputConnection(TopLevelImpl toplevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true)
{
_toplevel = toplevel;
_inputMethod = inputMethod;
_editable = new EditableWrapper(this);
}
public override bool EndBatchEdit()
{
var ret = base.EndBatchEdit();
_editable.EndBatchEdit();
public bool IsComposing => !string.IsNullOrEmpty(_compositionText);
return ret;
}
public int ExtractedTextToken { get; private set; }
public override IEditable Editable => _editable;
public EditableWrapper EditableWrapper => _editable;
public IAndroidInputMethod InputMethod => _inputMethod;
public override bool FinishComposingText()
public override bool SetComposingText(ICharSequence text, int newCursorPosition)
{
var ret = base.FinishComposingText();
InputEditable.RaiseCompositionChanged();
return ret;
_compositionText = text.SubSequence(0, text.Length());
System.Diagnostics.Debug.WriteLine($"Composition Changed: {_compositionText}");
if(_inputMethod.IsActive && !_commitInProgress)
{
_inputMethod.Client.SetPreeditText(_compositionText);
}
return base.SetComposingText(text, newCursorPosition);
}
public override bool CommitText(ICharSequence text, int newCursorPosition)
{
_commitInProgress = true;
var ret = base.CommitText(text, newCursorPosition);
InputEditable.RaiseCompositionChanged();
var committedText = text.SubSequence(0, text.Length());
if (string.IsNullOrEmpty(committedText))
{
committedText = _compositionText;
}
if (_inputMethod.IsActive && !string.IsNullOrEmpty(committedText))
{
if (!string.IsNullOrEmpty(_compositionText))
{
_inputMethod.Client.SetPreeditText(null);
}
_toplevel.TextInput(committedText);
_compositionText = null;
}
_commitInProgress = false;
return ret;
}
@ -517,5 +564,48 @@ namespace Avalonia.Android.Platform.SkiaPlatform
return base.PerformEditorAction(actionCode);
}
public override ExtractedText GetExtractedText(ExtractedTextRequest request, [GeneratedEnum] GetTextFlags flags)
{
if (request == null)
return null;
ExtractedTextToken = request.Token;
var editable = Editable;
if (editable == null)
{
return null;
}
if (!_inputMethod.IsActive)
{
return null;
}
var selection = _inputMethod.Client.Selection;
ExtractedText extract = new ExtractedText
{
Flags = 0,
PartialStartOffset = -1,
PartialEndOffset = -1,
SelectionStart = selection.Start,
SelectionEnd = selection.End,
StartOffset = 0
};
if ((request.Flags & GetTextFlags.WithStyles) != 0)
{
extract.Text = new SpannableString(editable);
}
else
{
extract.Text = editable;
}
return extract;
}
}
}

23
src/Avalonia.Base/Input/TextInput/ITextEditable.cs

@ -1,23 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Input.TextInput
{
[NotClientImplementable]
public interface ITextEditable
{
event EventHandler TextChanged;
event EventHandler SelectionChanged;
event EventHandler CompositionChanged;
int SelectionStart { get; set; }
int SelectionEnd { get; set; }
int CompositionStart { get; }
int CompositionEnd { get; }
string? Text { get; set; }
}
}

66
src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs

@ -1,66 +0,0 @@
using System;
using Avalonia.Media.TextFormatting;
using Avalonia.VisualTree;
namespace Avalonia.Input.TextInput
{
public interface ITextInputMethodClient
{
/// <summary>
/// The cursor rectangle relative to the TextViewVisual
/// </summary>
Rect CursorRectangle { get; }
/// <summary>
/// Should be fired when cursor rectangle is changed inside the TextViewVisual
/// </summary>
event EventHandler? CursorRectangleChanged;
/// <summary>
/// The visual that's showing the text
/// </summary>
Visual TextViewVisual { get; }
/// <summary>
/// Should be fired when text-hosting visual is changed
/// </summary>
event EventHandler? TextViewVisualChanged;
/// <summary>
/// Indicates if TextViewVisual is capable of displaying non-committed input on the cursor position
/// </summary>
bool SupportsPreedit { get; }
/// <summary>
/// Sets the non-committed input string
/// </summary>
void SetPreeditText(string? text);
/// <summary>
/// Sets the current composing region. This doesn't remove the composing text from the commited text.
/// </summary>
void SetComposingRegion(TextRange? region);
/// <summary>
/// Indicates if text input client is capable of providing the text around the cursor
/// </summary>
bool SupportsSurroundingText { get; }
/// <summary>
/// Returns the text around the cursor, usually the current paragraph, the cursor position inside that text and selection start position
/// </summary>
TextInputMethodSurroundingText SurroundingText { get; }
/// <summary>
/// Should be fired when surrounding text changed
/// </summary>
event EventHandler? SurroundingTextChanged;
/// <summary>
/// Gets or sets a platform editable. Text and selection changes made in the editable are forwarded to the IM client.
/// </summary>
ITextEditable? TextEditable { get; set; }
void SelectInSurroundingText(int start, int end);
}
public record struct TextInputMethodSurroundingText
{
public string Text { get; set; }
public int CursorOffset { get; set; }
public int AnchorOffset { get; set; }
}
}

2
src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs

@ -5,7 +5,7 @@ namespace Avalonia.Input.TextInput
[Unstable]
public interface ITextInputMethodImpl
{
void SetClient(ITextInputMethodClient? client);
void SetClient(TextInputMethodClient? client);
void SetCursorRect(Rect rect);
void SetOptions(TextInputOptions options);
void Reset();

4
src/Avalonia.Base/Input/TextInput/InputMethodManager.cs

@ -7,7 +7,7 @@ namespace Avalonia.Input.TextInput
{
private ITextInputMethodImpl? _im;
private IInputElement? _focusedElement;
private ITextInputMethodClient? _client;
private TextInputMethodClient? _client;
private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper();
public TextInputMethodManager()
@ -16,7 +16,7 @@ namespace Avalonia.Input.TextInput
InputMethod.IsInputMethodEnabledProperty.Changed.Subscribe(OnIsInputMethodEnabledChanged);
}
private ITextInputMethodClient? Client
private TextInputMethodClient? Client
{
get => _client;
set

136
src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs

@ -0,0 +1,136 @@
using System;
namespace Avalonia.Input.TextInput
{
public abstract class TextInputMethodClient
{
private Rect _cursorRectangle;
private string _surroundingText = "";
private TextSelection _selection;
/// <summary>
/// Fires when the text view visual has changed
/// </summary>
public event EventHandler? TextViewVisualChanged;
/// <summary>
/// Fires when the cursor rectangle has changed
/// </summary>
public event EventHandler? CursorRectangleChanged;
/// <summary>
/// Fires when the surrounding text has changed
/// </summary>
public event EventHandler? SurroundingTextChanged;
/// <summary>
/// Fires when the selection has changed
/// </summary>
public event EventHandler? SelectionChanged;
/// <summary>
/// The visual that's showing the text
/// </summary>
public abstract Visual TextViewVisual { get; }
/// <summary>
/// Indicates if TextViewVisual is capable of displaying non-committed input on the cursor position
/// </summary>
public abstract bool SupportsPreedit { get; }
/// <summary>
/// Indicates if text input client is capable of providing the text around the cursor
/// </summary>
public abstract bool SupportsSurroundingText { get; }
/// <summary>
/// Returns the text around the cursor, usually the current paragraph
/// </summary>
public string SurroundingText
{
get => _surroundingText;
set
{
var oldValue = _surroundingText;
if (oldValue == value)
{
return;
}
_surroundingText = value;
OnSurroundingTextChanged(oldValue, value);
}
}
/// <summary>
/// Gets the cursor rectangle relative to the TextViewVisual
/// </summary>
public Rect CursorRectangle
{
get => _cursorRectangle;
protected set
{
var oldvalue = _cursorRectangle;
if (oldvalue == value)
{
return;
}
_cursorRectangle = value;
OnCursorRectangleChanged(oldvalue, value);
}
}
/// <summary>
/// Gets or sets the curent selection range within current surrounding text.
/// </summary>
public TextSelection Selection
{
get => _selection;
set
{
var oldValue = _selection;
if (oldValue == value)
{
return;
}
_selection = value;
OnSelectionChanged(oldValue, value);
}
}
/// <summary>
/// Sets the non-committed input string
/// </summary>
public virtual void SetPreeditText(string? preeditText) { }
protected virtual void OnCursorRectangleChanged(Rect oldValue, Rect newValue)
{
CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnTextViewVisualChanged(Visual? oldValue, Visual? newValue)
{
TextViewVisualChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnSurroundingTextChanged(string? oldValue, string? newValue)
{
SurroundingTextChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnSelectionChanged(TextSelection oldValue, TextSelection newValue)
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
}
public record struct TextSelection(int Start, int End);
}

2
src/Avalonia.Base/Input/TextInput/TextInputMethodClientRequestedEventArgs.cs

@ -7,6 +7,6 @@ namespace Avalonia.Input.TextInput
/// <summary>
/// Set this property to a valid text input client to enable input method interaction
/// </summary>
public ITextInputMethodClient? Client { get; set; }
public TextInputMethodClient? Client { get; set; }
}
}

94
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -50,15 +50,6 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<string?> PreeditTextProperty =
AvaloniaProperty.Register<TextPresenter, string?>(nameof(PreeditText));
/// <summary>
/// Defines the <see cref="CompositionRegion"/> property.
/// </summary>
public static readonly DirectProperty<TextPresenter, TextRange?> CompositionRegionProperty =
AvaloniaProperty.RegisterDirect<TextPresenter, TextRange?>(
nameof(CompositionRegion),
o => o.CompositionRegion,
(o, v) => o.CompositionRegion = v);
/// <summary>
/// Defines the <see cref="TextAlignment"/> property.
/// </summary>
@ -97,7 +88,6 @@ namespace Avalonia.Controls.Presenters
private CharacterHit _lastCharacterHit;
private Rect _caretBounds;
private Point _navigationPosition;
private TextRange? _compositionRegion;
static TextPresenter()
{
@ -137,12 +127,6 @@ namespace Avalonia.Controls.Presenters
set => SetValue(PreeditTextProperty, value);
}
public TextRange? CompositionRegion
{
get => _compositionRegion;
set => SetAndRaise(CompositionRegionProperty, ref _compositionRegion, value);
}
/// <summary>
/// Gets or sets the font family.
/// </summary>
@ -490,10 +474,10 @@ namespace Avalonia.Controls.Presenters
{
TextLayout result;
var text = Text;
var caretIndex = CaretIndex;
var preeditText = PreeditText;
var text = GetCombinedText(Text, caretIndex, preeditText);
var typeface = new Typeface(FontFamily, FontStyle, FontWeight);
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd);
@ -503,22 +487,9 @@ namespace Avalonia.Controls.Presenters
var foreground = Foreground;
if (_compositionRegion != null)
{
var preeditHighlight = new ValueSpan<TextRunProperties>(_compositionRegion?.Start ?? 0, _compositionRegion?.Length ?? 0,
new GenericTextRunProperties(typeface, FontSize,
foregroundBrush: foreground,
textDecorations: TextDecorations.Underline));
textStyleOverrides = new[]
{
preeditHighlight
};
}
else if (!string.IsNullOrEmpty(PreeditText))
if (!string.IsNullOrEmpty(preeditText))
{
var preeditHighlight = new ValueSpan<TextRunProperties>(CaretIndex, PreeditText.Length,
var preeditHighlight = new ValueSpan<TextRunProperties>(caretIndex, preeditText.Length,
new GenericTextRunProperties(typeface, FontSize,
foregroundBrush: foreground,
textDecorations: TextDecorations.Underline));
@ -554,6 +525,27 @@ namespace Avalonia.Controls.Presenters
return result;
}
private static string? GetCombinedText(string? text, int caretIndex, string? preeditText)
{
if (string.IsNullOrEmpty(preeditText))
{
return text;
}
if (string.IsNullOrEmpty(text))
{
return preeditText;
}
var sb = StringBuilderCache.Acquire(text.Length + preeditText.Length);
sb.Append(text.Substring(0, caretIndex));
sb.Insert(caretIndex, preeditText);
sb.Append(text.Substring(caretIndex));
return StringBuilderCache.GetStringAndRelease(sb);
}
protected virtual void InvalidateTextLayout()
{
_textLayout?.Dispose();
@ -831,6 +823,18 @@ namespace Avalonia.Controls.Presenters
_caretTimer.Tick -= CaretTimerTick;
}
private void OnPreeditTextChanged(string? preeditText)
{
if (string.IsNullOrEmpty(preeditText))
{
UpdateCaret(new CharacterHit(CaretIndex), false);
}
else
{
UpdateCaret(new CharacterHit(CaretIndex + preeditText.Length), false);
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
@ -840,10 +844,30 @@ namespace Avalonia.Controls.Presenters
MoveCaretToTextPosition(change.GetNewValue<int>());
}
if(change.Property == PreeditTextProperty)
{
OnPreeditTextChanged(change.NewValue as string);
}
if(change.Property == TextProperty)
{
if (!string.IsNullOrEmpty(PreeditText))
{
PreeditText = null;
}
}
if(change.Property == CaretIndexProperty)
{
if (!string.IsNullOrEmpty(PreeditText))
{
PreeditText = null;
}
}
switch (change.Property.Name)
{
case nameof(PreeditText):
case nameof(CompositionRegion):
case nameof(Foreground):
case nameof(FontSize):
case nameof(FontStyle):

4
src/Avalonia.Controls/TextBox.cs

@ -975,7 +975,9 @@ namespace Avalonia.Controls
textBuilder.Insert(caretIndex, input);
SetCurrentValue(TextProperty, StringBuilderCache.GetStringAndRelease(textBuilder));
var text = StringBuilderCache.GetStringAndRelease(textBuilder);
SetCurrentValue(TextProperty, text);
ClearSelection();

292
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -1,132 +1,80 @@
using System;
using System.Diagnostics;
using Avalonia.Controls.Presenters;
using Avalonia.Input.TextInput;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.Controls
{
internal class TextBoxTextInputMethodClient : ITextInputMethodClient
internal class TextBoxTextInputMethodClient : TextInputMethodClient
{
private TextBox? _parent;
private TextPresenter? _presenter;
private ITextEditable? _textEditable;
private bool _isPropertyChange;
public Visual TextViewVisual => _presenter!;
public override Visual TextViewVisual => _presenter!;
public bool SupportsPreedit => true;
public override bool SupportsPreedit => true;
public bool SupportsSurroundingText => true;
public override bool SupportsSurroundingText => true;
public Rect CursorRectangle
public void SetPresenter(TextPresenter? presenter, TextBox? parent)
{
get
if (_parent != null)
{
if (_parent == null || _presenter == null)
{
return default;
}
var transform = _presenter.TransformToVisual(_parent);
if (transform == null)
{
return default;
}
var rect = _presenter.GetCursorRectangle().TransformToAABB(transform.Value);
return rect;
_parent.PropertyChanged -= OnParentPropertyChanged;
}
}
public TextInputMethodSurroundingText SurroundingText
{
get
{
if (_presenter is null || _parent is null)
{
return default;
}
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false);
var textLine = _presenter.TextLayout.TextLines[lineIndex];
var lineStart = textLine.FirstTextSourceIndex;
_parent = parent;
var lineText = GetTextLineText(textLine);
if (_parent != null)
{
_parent.PropertyChanged += OnParentPropertyChanged;
}
var anchorOffset = Math.Max(0, _parent.SelectionStart - lineStart);
var oldPresenter = _presenter;
var cursorOffset = Math.Max(0, _presenter.SelectionEnd - lineStart);
if (oldPresenter != null)
{
oldPresenter.ClearValue(TextPresenter.PreeditTextProperty);
return new TextInputMethodSurroundingText
{
Text = lineText ?? "",
AnchorOffset = anchorOffset,
CursorOffset = cursorOffset
};
oldPresenter.CaretBoundsChanged -= OnPresenterCursorRectangleChanged;
}
}
public ITextEditable? TextEditable
{
get => _textEditable; set
_presenter = presenter;
if (_presenter != null)
{
if (_textEditable != null)
{
_textEditable.TextChanged -= TextEditable_TextChanged;
_textEditable.SelectionChanged -= TextEditable_SelectionChanged;
_textEditable.CompositionChanged -= TextEditable_CompositionChanged;
}
_presenter.CaretBoundsChanged += OnPresenterCursorRectangleChanged;
}
_textEditable = value;
OnTextViewVisualChanged(oldPresenter, presenter);
if (_textEditable != null)
{
_textEditable.TextChanged += TextEditable_TextChanged;
_textEditable.SelectionChanged += TextEditable_SelectionChanged;
_textEditable.CompositionChanged += TextEditable_CompositionChanged;
if (_presenter != null)
{
_textEditable.Text = _presenter.Text;
_textEditable.SelectionStart = _presenter.SelectionStart;
_textEditable.SelectionEnd = _presenter.SelectionEnd;
}
}
}
OnPresenterCursorRectangleChanged(this, EventArgs.Empty);
}
private void TextEditable_CompositionChanged(object? sender, EventArgs e)
public override void SetPreeditText(string? preeditText)
{
if (_presenter != null && _textEditable != null)
if (_presenter == null || _parent == null)
{
_presenter.SetCurrentValue(TextPresenter.CompositionRegionProperty, new TextRange(_textEditable.CompositionStart, _textEditable.CompositionEnd));
return;
}
_presenter.SetCurrentValue(TextPresenter.PreeditTextProperty, preeditText);
}
private void TextEditable_SelectionChanged(object? sender, EventArgs e)
protected override void OnSelectionChanged(TextSelection oldValue, TextSelection newValue)
{
if (_parent != null && _textEditable != null)
base.OnSelectionChanged(oldValue, newValue);
if (_isPropertyChange)
{
_parent.SelectionStart = _textEditable.SelectionStart;
_parent.SelectionEnd = _textEditable.SelectionEnd;
return;
}
}
private void TextEditable_TextChanged(object? sender, EventArgs e)
{
if (_parent != null)
if (oldValue != newValue)
{
if (_parent.Text != _textEditable?.Text)
{
_parent.Text = _textEditable?.Text;
}
SetParentSelection(newValue);
}
}
@ -153,166 +101,112 @@ namespace Avalonia.Controls
return lineText;
}
public event EventHandler? TextViewVisualChanged;
public event EventHandler? CursorRectangleChanged;
public event EventHandler? SurroundingTextChanged;
private string? _presenterText;
private int _compositionStart;
public void SetPreeditText(string? preeditText)
private void OnParentTextChanged()
{
if (_presenter == null || _parent == null)
if (_presenter is null || _parent is null)
{
return;
}
SurroundingText = "";
if (_presenterText is null)
{
_presenterText = _parent.Text ?? "";
_compositionStart = _parent.CaretIndex;
return;
}
var text = GetText(preeditText);
_presenter.SetCurrentValue(TextPresenter.TextProperty, text);
_presenter.SetCurrentValue(TextPresenter.PreeditTextProperty, preeditText);
_presenter.UpdateCaret(new CharacterHit(_compositionStart + (preeditText != null ? preeditText.Length : 0)), false);
if (string.IsNullOrEmpty(preeditText))
#if DEBUG
if (_parent.CaretIndex != _presenter.CaretIndex)
{
_presenterText = null;
throw new InvalidOperationException("TextBox and TextPresenter are out of sync");
}
}
private string? GetText(string? preeditText)
{
if (string.IsNullOrEmpty(preeditText))
if (_parent.Text != _presenter.Text)
{
return _presenterText;
throw new InvalidOperationException("TextBox and TextPresenter are out of sync");
}
#endif
if (string.IsNullOrEmpty(_presenterText))
{
return preeditText;
}
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false);
var sb = StringBuilderCache.Acquire(_presenterText.Length + preeditText.Length);
var textLine = _presenter.TextLayout.TextLines[lineIndex];
sb.Append(_presenterText);
sb.Insert(_compositionStart, preeditText);
var lineText = GetTextLineText(textLine);
return StringBuilderCache.GetStringAndRelease(sb);
SurroundingText = lineText;
}
public void SetComposingRegion(TextRange? region)
private void OnPresenterCursorRectangleChanged(object? sender, EventArgs e)
{
if (_presenter == null)
if (_parent == null || _presenter == null)
{
CursorRectangle = default;
return;
}
_presenter.SetCurrentValue(TextPresenter.CompositionRegionProperty, region);
}
var transform = _presenter.TransformToVisual(_parent);
public void SelectInSurroundingText(int start, int end)
{
if (_parent is null || _presenter is null)
if (transform == null)
{
CursorRectangle = default;
return;
}
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_presenter.CaretIndex, false);
var textLine = _presenter.TextLayout.TextLines[lineIndex];
var lineStart = textLine.FirstTextSourceIndex;
var selectionStart = lineStart + start;
var selectionEnd = lineStart + end;
_parent.SelectionStart = selectionStart;
_parent.SelectionEnd = selectionEnd;
CursorRectangle = _presenter.GetCursorRectangle().TransformToAABB(transform.Value);
}
public void SetPresenter(TextPresenter? presenter, TextBox? parent)
private void OnParentPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (_parent != null)
_isPropertyChange = true;
if (e.Property == TextBox.TextProperty)
{
_parent.PropertyChanged -= OnParentPropertyChanged;
OnParentTextChanged();
}
_parent = parent;
if (_parent != null)
if (e.Property == TextBox.SelectionStartProperty || e.Property == TextBox.SelectionEndProperty)
{
_parent.PropertyChanged += OnParentPropertyChanged;
Selection = GetParentSelection();
}
if (_presenter != null)
_isPropertyChange = false;
}
private TextSelection GetParentSelection()
{
if (_presenter is null || _parent is null)
{
_presenter.ClearValue(TextPresenter.PreeditTextProperty);
return default;
}
_presenter.ClearValue(TextPresenter.CompositionRegionProperty);
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_parent.CaretIndex, false);
_presenter.CaretBoundsChanged -= OnCaretBoundsChanged;
}
var textLine = _presenter.TextLayout.TextLines[lineIndex];
_presenter = presenter;
var lineStart = textLine.FirstTextSourceIndex;
if (_presenter != null)
{
_presenter.CaretBoundsChanged += OnCaretBoundsChanged;
}
var selectionStart = Math.Max(0, _parent.SelectionStart - lineStart);
TextViewVisualChanged?.Invoke(this, EventArgs.Empty);
var selectionEnd = Math.Max(0, _parent.SelectionEnd - lineStart);
OnCaretBoundsChanged(this, EventArgs.Empty);
return new TextSelection(selectionStart, selectionEnd);
}
private void OnParentPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
private void SetParentSelection(TextSelection selection)
{
if (e.Property == TextBox.SelectionStartProperty || e.Property == TextBox.SelectionEndProperty)
if (_parent is null || _presenter is null)
{
if (SupportsSurroundingText)
{
SurroundingTextChanged?.Invoke(this, e);
}
if (_textEditable != null)
{
var value = (int)(e.NewValue ?? 0);
if (e.Property == TextBox.SelectionStartProperty)
{
_textEditable.SelectionStart = value;
}
if (e.Property == TextBox.SelectionEndProperty)
{
_textEditable.SelectionEnd = value;
}
}
return;
}
if (e.Property == TextBox.TextProperty)
{
if (_textEditable != null)
{
_textEditable.Text = (string?)e.NewValue;
}
}
}
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(_parent.CaretIndex, false);
private void OnCaretBoundsChanged(object? sender, EventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
CursorRectangleChanged?.Invoke(this, e);
var textLine = _presenter.TextLayout.TextLines[lineIndex];
var lineStart = textLine.FirstTextSourceIndex;
var selectionStart = lineStart + selection.Start;
var selectionEnd = lineStart + selection.End;
}, DispatcherPriority.Input);
_parent.SelectionStart = selectionStart;
_parent.SelectionEnd = selectionEnd;
}
}
}

6
src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs

@ -41,7 +41,7 @@ namespace Avalonia.FreeDesktop.DBusIme
private PixelRect? _lastReportedRect;
private double _scaling = 1;
private PixelPoint _windowPosition;
private ITextInputMethodClient? _client;
private TextInputMethodClient? _client;
protected bool IsConnected => _currentName != null;
@ -53,7 +53,7 @@ namespace Avalonia.FreeDesktop.DBusIme
_ = WatchAsync();
}
public ITextInputMethodClient Client => _client;
public TextInputMethodClient Client => _client;
public bool IsActive => _client is not null;
@ -210,7 +210,7 @@ namespace Avalonia.FreeDesktop.DBusIme
UpdateActive();
}
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client)
void ITextInputMethodImpl.SetClient(TextInputMethodClient? client)
{
_client = client;
UpdateActive();

18
src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Input.TextInput;
using Avalonia.Media.TextFormatting;
using Avalonia.Native.Interop;
#nullable enable
@ -8,7 +9,7 @@ namespace Avalonia.Native
{
internal class AvaloniaNativeTextInputMethod : ITextInputMethodImpl, IDisposable
{
private ITextInputMethodClient? _client;
private TextInputMethodClient? _client;
private IAvnTextInputMethodClient? _nativeClient;
private readonly IAvnTextInputMethod _inputMethod;
@ -28,7 +29,7 @@ namespace Avalonia.Native
_inputMethod.Reset();
}
public void SetClient(ITextInputMethodClient? client)
public void SetClient(TextInputMethodClient? client)
{
if (_client is { SupportsSurroundingText: true })
{
@ -96,11 +97,12 @@ namespace Avalonia.Native
}
var surroundingText = _client.SurroundingText;
var selection = _client.Selection;
_inputMethod.SetSurroundingText(
surroundingText.Text ?? "",
surroundingText.AnchorOffset,
surroundingText.CursorOffset
surroundingText ?? "",
selection.Start,
selection.End
);
}
@ -116,9 +118,9 @@ namespace Avalonia.Native
private class AvnTextInputMethodClient : NativeCallbackBase, IAvnTextInputMethodClient
{
private readonly ITextInputMethodClient _client;
private readonly TextInputMethodClient _client;
public AvnTextInputMethodClient(ITextInputMethodClient client)
public AvnTextInputMethodClient(TextInputMethodClient client)
{
_client = client;
}
@ -135,7 +137,7 @@ namespace Avalonia.Native
{
if (_client.SupportsSurroundingText)
{
_client.SelectInSurroundingText(start, end);
_client.Selection = new TextSelection(start, end);
}
}
}

6
src/Avalonia.X11/X11Window.Xim.cs

@ -15,14 +15,14 @@ namespace Avalonia.X11
private readonly X11Window _parent;
private bool _windowActive, _imeActive;
private Rect? _queuedCursorRect;
private ITextInputMethodClient? _client;
private TextInputMethodClient? _client;
public XimInputMethod(X11Window parent)
{
_parent = parent;
}
public ITextInputMethodClient? Client => _client;
public TextInputMethodClient? Client => _client;
public bool IsActive => _client != null;
@ -62,7 +62,7 @@ namespace Avalonia.X11
UpdateActive();
}
public void SetClient(ITextInputMethodClient client)
public void SetClient(TextInputMethodClient client)
{
_client = client;
UpdateActive();

18
src/Browser/Avalonia.Browser/AvaloniaView.cs

@ -42,7 +42,7 @@ namespace Avalonia.Browser
private const SKColorType ColorType = SKColorType.Rgba8888;
private bool _useGL;
private ITextInputMethodClient? _client;
private TextInputMethodClient? _client;
/// <param name="divId">ID of the html element where avalonia content should be rendered.</param>
public AvaloniaView(string divId)
@ -397,7 +397,7 @@ namespace Avalonia.Browser
if(start != -1 && end != -1 && _client != null)
{
_client.SelectInSurroundingText(start, end);
_client.Selection = new TextSelection(start, end);
}
return false;
}
@ -513,7 +513,7 @@ namespace Avalonia.Browser
InputHelper.FocusElement(_containerElement);
}
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client)
void ITextInputMethodImpl.SetClient(TextInputMethodClient? client)
{
if (_client != null)
{
@ -534,9 +534,10 @@ namespace Avalonia.Browser
InputHelper.ShowElement(_inputElement);
InputHelper.FocusElement(_inputElement);
var surroundingText = _client.SurroundingText;
var surroundingText = _client.SurroundingText ?? "";
var selection = _client.Selection;
InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
InputHelper.SetSurroundingText(_inputElement, surroundingText, selection.Start, selection.End);
}
else
{
@ -548,16 +549,17 @@ namespace Avalonia.Browser
{
if (_client != null)
{
var surroundingText = _client.SurroundingText;
var surroundingText = _client.SurroundingText ?? "";
var selection = _client.Selection;
InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
InputHelper.SetSurroundingText(_inputElement, surroundingText, selection.Start, selection.End);
}
}
void ITextInputMethodImpl.SetCursorRect(Rect rect)
{
InputHelper.FocusElement(_inputElement);
InputHelper.SetBounds(_inputElement, (int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height, _client?.SurroundingText.CursorOffset ?? 0);
InputHelper.SetBounds(_inputElement, (int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height, _client?.Selection.End ?? 0);
InputHelper.FocusElement(_inputElement);
}

4
src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs

@ -26,7 +26,7 @@ namespace Avalonia.Win32.Input
private bool _ignoreComposition;
public ITextInputMethodClient? Client { get; private set; }
public TextInputMethodClient? Client { get; private set; }
[MemberNotNullWhen(true, nameof(Client))]
public bool IsActive => Client != null;
@ -145,7 +145,7 @@ namespace Avalonia.Win32.Input
});
}
public void SetClient(ITextInputMethodClient? client)
public void SetClient(TextInputMethodClient? client)
{
if(Client != null)
{

2
src/iOS/Avalonia.iOS/AvaloniaView.Text.cs

@ -29,7 +29,7 @@ public partial class AvaloniaView
private bool IsDrivingText => CurrentAvaloniaResponder is TextInputResponder t && ReferenceEquals(t.NextResponder, this);
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client)
void ITextInputMethodImpl.SetClient(TextInputMethodClient? client)
{
_client = client;
if (_client == null && IsDrivingText)

2
src/iOS/Avalonia.iOS/AvaloniaView.cs

@ -27,7 +27,7 @@ namespace Avalonia.iOS
private TopLevelImpl _topLevelImpl;
private EmbeddableControlRoot _topLevel;
private TouchHandler _touches;
private ITextInputMethodClient _client;
private TextInputMethodClient _client;
private IAvaloniaViewController _controller;
public AvaloniaView()

36
src/iOS/Avalonia.iOS/TextInputResponder.cs

@ -78,7 +78,7 @@ partial class AvaloniaView
public NSObject Copy(NSZone? zone) => this;
}
public TextInputResponder(AvaloniaView view, ITextInputMethodClient client)
public TextInputResponder(AvaloniaView view, TextInputMethodClient client)
{
_view = view;
NextResponder = view;
@ -88,12 +88,12 @@ partial class AvaloniaView
public override UIResponder NextResponder { get; }
private readonly ITextInputMethodClient _client;
private readonly TextInputMethodClient _client;
private int _inSurroundingTextUpdateEvent;
private readonly UITextPosition _beginningOfDocument = new AvaloniaTextPosition(0);
private readonly UITextInputStringTokenizer _tokenizer;
public ITextInputMethodClient? Client => _client;
public TextInputMethodClient? Client => _client;
public override bool CanResignFirstResponder => true;
@ -201,17 +201,23 @@ partial class AvaloniaView
string IUITextInput.TextInRange(UITextRange range)
{
var r = (AvaloniaTextRange)range;
var s = _client.SurroundingText;
var surroundingText = _client.SurroundingText;
var currentSelection = _client.Selection;
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.TextInRange {start} {end}", r.StartIndex, r.EndIndex);
string result = "";
if (string.IsNullOrEmpty(_markedText))
result = s.Text[r.StartIndex..r.EndIndex];
if(surroundingText != null && r.EndIndex < surroundingText.Length)
{
result = surroundingText[r.StartIndex..r.EndIndex];
}
else
{
var span = new CombinedSpan3<char>(s.Text.AsSpan().Slice(0, s.CursorOffset),
var span = new CombinedSpan3<char>(surroundingText.AsSpan().Slice(0, currentSelection.Start),
_markedText,
s.Text.AsSpan().Slice(s.CursorOffset));
surroundingText.AsSpan().Slice(currentSelection.Start));
var buf = new char[r.EndIndex - r.StartIndex];
span.CopyTo(buf, r.StartIndex);
result = new string(buf);
@ -226,7 +232,7 @@ partial class AvaloniaView
var r = (AvaloniaTextRange)range;
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUIKeyInput.ReplaceText {start} {end} {text}", r.StartIndex, r.EndIndex, text);
_client.SelectInSurroundingText(r.StartIndex, r.EndIndex);
_client.Selection = new TextSelection(r.StartIndex, r.EndIndex);
TextInput(text);
}
@ -447,21 +453,19 @@ partial class AvaloniaView
{
get
{
return new AvaloniaTextRange(
Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset),
Math.Max(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset));
return new AvaloniaTextRange(_client.Selection.Start, _client.Selection.End);
}
set
{
if (_inSurroundingTextUpdateEvent > 0)
return;
if (value == null)
_client.SelectInSurroundingText(_client.SurroundingText.CursorOffset,
_client.SurroundingText.CursorOffset);
_client.Selection = default;
else
{
var r = (AvaloniaTextRange)value;
_client.SelectInSurroundingText(r.StartIndex, r.EndIndex);
_client.Selection = new TextSelection(r.StartIndex, r.EndIndex);
}
}
}
@ -474,7 +478,7 @@ partial class AvaloniaView
UITextPosition IUITextInput.BeginningOfDocument => _beginningOfDocument;
private int DocumentLength => (_client.SurroundingText.Text?.Length ?? 0) + (_markedText?.Length ?? 0);
private int DocumentLength => (_client.SurroundingText?.Length ?? 0) + (_markedText?.Length ?? 0);
UITextPosition IUITextInput.EndOfDocument => new AvaloniaTextPosition(DocumentLength);
UITextRange IUITextInput.MarkedTextRange
@ -483,7 +487,7 @@ partial class AvaloniaView
{
if (string.IsNullOrWhiteSpace(_markedText))
return null!;
return new AvaloniaTextRange(_client.SurroundingText.CursorOffset, _client.SurroundingText.CursorOffset + _markedText.Length);
return new AvaloniaTextRange(_client.Selection.Start, _client.Selection.Start + _markedText.Length);
}
}

Loading…
Cancel
Save