浏览代码

Rework ITextInputMethodClient

Benedikt Stebner 2 年之前
父节点
当前提交
be9a26cbb1

+ 14 - 1
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 - 0
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" />

+ 46 - 24
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 - 0
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" />

+ 0 - 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);
-        }
-    }
-}

+ 131 - 41
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;
+        }
     }
 }

+ 0 - 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; }
-    }
-}

+ 0 - 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; }
-    }
-}

+ 1 - 1
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();

+ 2 - 2
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 - 0
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);
+}

+ 1 - 1
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; }
     }
 }

+ 59 - 35
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):

+ 3 - 1
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();
 

+ 93 - 199
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;
         }
     }
 }

+ 3 - 3
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();

+ 10 - 8
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);
                 }
             }
         }

+ 3 - 3
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();

+ 10 - 8
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);
         }
 

+ 2 - 2
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)
             {

+ 1 - 1
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)

+ 1 - 1
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()

+ 20 - 16
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);
             }
         }