Просмотр исходного кода

Implemented basic IME support for Linux

Nikita Tsukanov 4 лет назад
Родитель
Сommit
35b365d97d
41 измененных файлов с 2099 добавлено и 136 удалено
  1. 2 1
      Avalonia.sln.DotSettings
  2. 2 1
      samples/ControlCatalog.NetCore/Program.cs
  3. BIN
      samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf
  4. 3 0
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  5. 11 0
      src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs
  6. 18 7
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  7. 6 1
      src/Avalonia.Controls/TextBox.cs
  8. 41 0
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  9. 6 0
      src/Avalonia.Controls/TopLevel.cs
  10. 110 0
      src/Avalonia.FreeDesktop/DBusCallQueue.cs
  11. 8 3
      src/Avalonia.FreeDesktop/DBusHelper.cs
  12. 288 0
      src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
  13. 69 0
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs
  14. 67 0
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs
  15. 51 0
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs
  16. 149 0
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
  17. 52 0
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs
  18. 45 0
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs
  19. 105 0
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs
  20. 53 0
      src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs
  21. 31 0
      src/Avalonia.FreeDesktop/IX11InputMethod.cs
  22. 35 0
      src/Avalonia.Input/InputElement.cs
  23. 6 0
      src/Avalonia.Input/KeyboardDevice.cs
  24. 60 0
      src/Avalonia.Input/TextInput/ITextInputMethodClient.cs
  25. 15 0
      src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs
  26. 101 0
      src/Avalonia.Input/TextInput/InputMethodManager.cs
  27. 12 0
      src/Avalonia.Input/TextInput/TextInputContentType.cs
  28. 12 0
      src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs
  29. 32 0
      src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs
  30. 109 0
      src/Avalonia.Input/TextInput/TransformTrackingHelper.cs
  31. 2 2
      src/Avalonia.X11/X11Clipboard.cs
  32. 2 2
      src/Avalonia.X11/X11Globals.cs
  33. 21 4
      src/Avalonia.X11/X11Info.cs
  34. 70 2
      src/Avalonia.X11/X11Platform.cs
  35. 8 3
      src/Avalonia.X11/X11PlatformThreading.cs
  36. 1 1
      src/Avalonia.X11/X11Screens.cs
  37. 76 54
      src/Avalonia.X11/X11Structs.cs
  38. 208 0
      src/Avalonia.X11/X11Window.Ime.cs
  39. 121 0
      src/Avalonia.X11/X11Window.Xim.cs
  40. 34 46
      src/Avalonia.X11/X11Window.cs
  41. 57 9
      src/Avalonia.X11/XLib.cs

+ 2 - 1
Avalonia.sln.DotSettings

@@ -38,4 +38,5 @@
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypeParameters/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="T" Suffix="" Style="AaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002EDaemon_002ESettings_002EMigration_002ESwaWarningsModeSettingsMigrate/@EntryIndexedValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/UserDictionary/Words/=Avalonia/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Avalonia/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Fcitx/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

+ 2 - 1
samples/ControlCatalog.NetCore/Program.cs

@@ -109,7 +109,8 @@ namespace ControlCatalog.NetCore
                 .With(new X11PlatformOptions
                 {
                     EnableMultiTouch = true,
-                    UseDBusMenu = true
+                    UseDBusMenu = true,
+                    EnableIme = true,
                 })
                 .With(new Win32PlatformOptions
                 {

BIN
samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf


+ 3 - 0
samples/ControlCatalog/Pages/TextBoxPage.xaml

@@ -64,5 +64,8 @@
         <TextBox Width="200" Text="Custom font italic bold" FontWeight="Bold" FontStyle="Italic" FontFamily="/Assets/Fonts/SourceSansPro-*.ttf#Source Sans Pro"/>
       </StackPanel>
     </StackPanel>
+    <TextBox AcceptsReturn="True" TextWrapping="Wrap" Height="200" MaxWidth="400"
+             FontFamily="avares://ControlCatalog/Assets/Fonts#WenQuanYi Micro Hei"
+             Text="计算机科学(是系统性研究信息与计算的理论基础以及它们在计算机系统中如何实现与应用的实用技术的学科。它通常被形容为对那些创造、描述以及转换信息的算法处理的系统研究。计算机科学包含很多分支领域;有些强调特定结果的计算,比如计算机图形学;而有些是探討计算问题的性质,比如计算复杂性理论;还有一些领域專注于怎样实现计算,比如程式語言理論是研究描述计算的方法,而程式设计是应用特定的程式語言解决特定的计算问题,人机交互则是專注于怎样使计算机和计算变得有用、好用,以及随时随地为人所用。&#xD;&#xD;有时公众会误以为计算机科学就是解决计算机问题的事业(比如信息技术),或者只是与使用计算机的经验有关,如玩游戏、上网或者文字处理。其实计算机科学所关注的,不仅仅是去理解实现类似游戏、浏览器这些软件的程序的性质,更要通过现有的知识创造新的程序或者改进已有的程序。" />
   </StackPanel>
 </UserControl>

+ 11 - 0
src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs

@@ -0,0 +1,11 @@
+using Avalonia.Input;
+using Avalonia.Input.TextInput;
+using Avalonia.Platform;
+
+namespace Avalonia.Controls.Platform
+{
+    public interface ITopLevelImplWithTextInputMethod : ITopLevelImpl
+    {
+        public ITextInputMethodImpl TextInputMethod { get; }
+    }
+}

+ 18 - 7
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Reactive.Linq;
+using Avalonia.Input.TextInput;
 using Avalonia.Media;
 using Avalonia.Metadata;
 using Avalonia.Threading;
@@ -378,19 +379,23 @@ namespace Avalonia.Controls.Presenters
 
                 if (_caretBlink)
                 {
-                    var charPos = FormattedText.HitTestTextPosition(CaretIndex);
-                    var x = Math.Floor(charPos.X) + 0.5;
-                    var y = Math.Floor(charPos.Y) + 0.5;
-                    var b = Math.Ceiling(charPos.Bottom) - 0.5;
-
+                    var (p1, p2) = GetCaretPoints();
                     context.DrawLine(
                         new Pen(caretBrush, 1),
-                        new Point(x, y),
-                        new Point(x, b));
+                        p1, p2);
                 }
             }
         }
 
+        (Point, Point) GetCaretPoints()
+        {
+            var charPos = FormattedText.HitTestTextPosition(CaretIndex);
+            var x = Math.Floor(charPos.X) + 0.5;
+            var y = Math.Floor(charPos.Y) + 0.5;
+            var b = Math.Ceiling(charPos.Bottom) - 0.5;
+            return (new Point(x, y), new Point(x, b));
+        }
+
         public void ShowCaret()
         {
             _caretBlink = true;
@@ -538,5 +543,11 @@ namespace Avalonia.Controls.Presenters
             _caretBlink = !_caretBlink;
             InvalidateVisual();
         }
+
+        internal Rect GetCursorRectangle()
+        {
+            var (p1, p2) = GetCaretPoints();
+            return new Rect(p1, p2);
+        }
     }
 }

+ 6 - 1
src/Avalonia.Controls/TextBox.cs

@@ -149,6 +149,7 @@ namespace Avalonia.Controls
         private int _selectionStart;
         private int _selectionEnd;
         private TextPresenter _presenter;
+        private TextBoxTextInputMethodClient _imClient = new TextBoxTextInputMethodClient();
         private UndoRedoHelper<UndoRedoState> _undoRedoHelper;
         private bool _isUndoingRedoing;
         private bool _ignoreTextChanges;
@@ -161,6 +162,10 @@ namespace Avalonia.Controls
         static TextBox()
         {
             FocusableProperty.OverrideDefaultValue(typeof(TextBox), true);
+            TextInputMethodClientRequestedEvent.AddClassHandler<TextBox>((tb, e) =>
+            {
+                e.Client = tb._imClient;
+            });
         }
 
         public TextBox()
@@ -437,7 +442,7 @@ namespace Avalonia.Controls
         protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
         {
             _presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
-
+            _imClient.SetPresenter(_presenter);
             if (IsFocused)
             {
                 _presenter?.ShowCaret();

+ 41 - 0
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@@ -0,0 +1,41 @@
+using System;
+using Avalonia.Controls.Presenters;
+using Avalonia.Input.TextInput;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls
+{
+    internal class TextBoxTextInputMethodClient : ITextInputMethodClient
+    {
+        private TextPresenter _presenter;
+        private IDisposable _subscription;
+        public Rect CursorRectangle => _presenter?.GetCursorRectangle() ?? default;
+        public event EventHandler CursorRectangleChanged;
+        public IVisual TextViewVisual => _presenter;
+        public event EventHandler TextViewVisualChanged;
+        public bool SupportsPreedit => false;
+        public void SetPreeditText(string text) => throw new NotSupportedException();
+
+        public bool SupportsSurroundingText => false;
+        public TextInputMethodSurroundingText SurroundingText => throw new NotSupportedException();
+        public event EventHandler SurroundingTextChanged;
+        public string TextBeforeCursor => null;
+        public string TextAfterCursor => null;
+
+        private void OnCaretIndexChanged(int index) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
+        
+        public void SetPresenter(TextPresenter presenter)
+        {
+            _subscription?.Dispose();
+            _subscription = null;
+            _presenter = presenter;
+            if (_presenter != null)
+            {
+                _subscription = _presenter.GetObservable(TextPresenter.CaretIndexProperty)
+                    .Subscribe(OnCaretIndexChanged);
+            }
+            TextViewVisualChanged?.Invoke(this, EventArgs.Empty);
+            CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
+        }
+    }
+}

+ 6 - 0
src/Avalonia.Controls/TopLevel.cs

@@ -1,8 +1,10 @@
 using System;
 using System.Reactive.Linq;
+using Avalonia.Controls.Platform;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
 using Avalonia.Layout;
 using Avalonia.Logging;
 using Avalonia.LogicalTree;
@@ -31,6 +33,7 @@ namespace Avalonia.Controls
         ICloseable,
         IStyleHost,
         ILogicalRoot,
+        ITextInputMethodRoot,
         IWeakSubscriber<ResourcesChangedEventArgs>
     {
         /// <summary>
@@ -489,5 +492,8 @@ namespace Avalonia.Controls
             if (focused == this)
                 KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
         }
+
+        ITextInputMethodImpl ITextInputMethodRoot.InputMethod =>
+            (PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod;
     }
 }

+ 110 - 0
src/Avalonia.FreeDesktop/DBusCallQueue.cs

@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Avalonia.FreeDesktop
+{
+    class DBusCallQueue
+    {
+        private readonly Func<Exception, Task> _errorHandler;
+
+        class Item
+        {
+            public Func<Task> Callback;
+            public Action<Exception> OnFinish;
+        }
+        private Queue<Item> _q = new Queue<Item>();
+        private bool _processing;
+
+        public DBusCallQueue(Func<Exception, Task> errorHandler)
+        {
+            _errorHandler = errorHandler;
+        }
+        
+        public void Enqueue(Func<Task> cb)
+        {
+            _q.Enqueue(new Item
+            {
+                Callback = cb
+            });
+            Process();
+        }
+
+        public Task EnqueueAsync(Func<Task> cb)
+        {
+            var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
+            _q.Enqueue(new Item
+            {
+                Callback = cb,
+                OnFinish = e =>
+                {
+                    if (e == null)
+                        tcs.TrySetResult(0);
+                    else
+                        tcs.TrySetException(e);
+                }
+            });
+            Process();
+            return tcs.Task;
+        }
+        
+        public Task<T> EnqueueAsync<T>(Func<Task<T>> cb)
+        {
+            var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
+            _q.Enqueue(new Item
+            {
+                Callback = async () =>
+                {
+                    var res = await cb();
+                    tcs.TrySetResult(res);
+                },
+                OnFinish = e =>
+                {
+                    if (e != null)
+                        tcs.TrySetException(e);
+                }
+            });
+            Process();
+            return tcs.Task;
+        }
+
+        async void Process()
+        {
+            if(_processing)
+                return;
+            _processing = true;
+            try
+            {
+                while (_q.Count > 0)
+                {
+                    var item = _q.Dequeue();
+                    try
+                    {
+                        await item.Callback();
+                        item.OnFinish?.Invoke(null);
+                    }
+                    catch(Exception e)
+                    {
+                        if (item.OnFinish != null)
+                            item.OnFinish(e);
+                        else
+                            await _errorHandler(e);
+                    }
+                }
+            }
+            finally
+            {
+                _processing = false;
+            }
+        }
+
+        public void FailAll()
+        {
+            while (_q.Count>0)
+            {
+                var item = _q.Dequeue();
+                item.OnFinish?.Invoke(new OperationCanceledException());
+            }
+        }
+    }
+}

+ 8 - 3
src/Avalonia.FreeDesktop/DBusHelper.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Threading;
+using Avalonia.Logging;
 using Avalonia.Threading;
 using Tmds.DBus;
 
@@ -48,8 +49,10 @@ namespace Avalonia.FreeDesktop
         }
         public static Connection Connection { get; private set; }
 
-        public static Exception TryInitialize(string dbusAddress = null)
+        public static Connection TryInitialize(string dbusAddress = null)
         {
+            if (Connection != null)
+                return Connection;
             var oldContext = SynchronizationContext.Current;
             try
             {
@@ -70,13 +73,15 @@ namespace Avalonia.FreeDesktop
             }
             catch (Exception e)
             {
-                return e;
+                Logger.TryGet(LogEventLevel.Error, "DBUS")
+                    ?.Log(null, "Unable to connect to DBus: " + e);
             }
             finally
             {
                 SynchronizationContext.SetSynchronizationContext(oldContext);
             }
-            return null;
+
+            return Connection;
         }
     }
 }

+ 288 - 0
src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs

@@ -0,0 +1,288 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Reflection;
+using System.Threading.Tasks;
+using Avalonia.FreeDesktop.DBusIme.Fcitx;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+using Avalonia.Logging;
+using Tmds.DBus;
+
+namespace Avalonia.FreeDesktop.DBusIme
+{
+    internal class DBusInputMethodFactory<T> : IX11InputMethodFactory where T : ITextInputMethodImpl, IX11InputMethodControl
+    {
+        private readonly Func<IntPtr, T> _factory;
+
+        public DBusInputMethodFactory(Func<IntPtr, T> factory)
+        {
+            _factory = factory;
+        }
+
+        public (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid)
+        {
+            var im = _factory(xid);
+            return (im, im);
+        }
+    }
+    
+    internal abstract class DBusTextInputMethodBase : IX11InputMethodControl, ITextInputMethodImpl
+    {
+        private List<IDisposable> _disposables = new List<IDisposable>();
+        private Queue<string> _onlineNamesQueue = new Queue<string>();
+        protected Connection Connection { get; }
+        private readonly string[] _knownNames;
+        private bool _connecting;
+        private string _currentName;
+        private DBusCallQueue _queue;
+        private bool _controlActive, _windowActive;
+        private bool? _imeActive;
+        private Rect _logicalRect;
+        private PixelRect? _lastReportedRect;
+        private double _scaling = 1;
+        private PixelPoint _windowPosition;
+        
+        protected bool IsConnected => _currentName != null;
+        
+        public DBusTextInputMethodBase(Connection connection, params string[] knownNames)
+        {
+            _queue = new DBusCallQueue(QueueOnError);
+            Connection = connection;
+            _knownNames = knownNames;
+            Watch();
+        }
+
+        async void Watch()
+        {
+            foreach (var name in _knownNames)
+                _disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange));
+        }
+        
+        protected abstract Task<bool> Connect(string name);
+
+        protected string GetAppName() =>
+            Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia";
+        
+        private async void OnNameChange(ServiceOwnerChangedEventArgs args)
+        {
+            if (args.NewOwner != null && _currentName == null)
+            {
+                _onlineNamesQueue.Enqueue(args.ServiceName);
+                if(!_connecting)
+                {
+                    _connecting = true;
+                    try
+                    {
+                        while (_onlineNamesQueue.Count > 0)
+                        {
+                            var name = _onlineNamesQueue.Dequeue();
+                            try
+                            {
+                                if (await Connect(name))
+                                {
+                                    _onlineNamesQueue.Clear();
+                                    _currentName = name;
+                                    return;
+                                }
+                            }
+                            catch (Exception e)
+                            {
+                                Logger.TryGet(LogEventLevel.Error, "IME")
+                                    ?.Log(this, "Unable to create IME input context:\n" + e);
+                            }
+                        }
+                    }
+                    finally
+                    {
+                        _connecting = false;
+                    }
+                }
+                
+            }
+            
+            // IME has crashed
+            if (args.NewOwner == null && args.ServiceName == _currentName)
+            {
+                _currentName = null;
+                foreach(var s in _disposables)
+                    s.Dispose();
+                _disposables.Clear();
+                
+                OnDisconnected();
+                Reset();
+                
+                // Watch again
+                Watch();
+            }
+        }
+        
+        protected virtual Task Disconnect()
+        {
+            return Task.CompletedTask;
+        }
+
+        protected virtual void OnDisconnected()
+        {
+            
+        }
+
+        protected virtual void Reset()
+        {
+            _lastReportedRect = null;
+            _imeActive = null;
+        }
+
+        async Task QueueOnError(Exception e)
+        {
+            Logger.TryGet(LogEventLevel.Error, "IME")
+                ?.Log(this, "Error:\n" + e);
+            try
+            {
+                await Disconnect();
+            }
+            catch (Exception ex)
+            {
+                Logger.TryGet(LogEventLevel.Error, "IME")
+                    ?.Log(this, "Error while destroying the context:\n" + ex);
+            }
+            OnDisconnected();
+            _currentName = null;
+        }
+        
+        protected void Enqueue(Func<Task> cb) => _queue.Enqueue(cb);
+
+        protected void AddDisposable(IDisposable d) => _disposables.Add(d);
+        
+        public void Dispose()
+        {
+            foreach(var d in _disposables)
+                d.Dispose();
+            _disposables.Clear();
+            try
+            {
+                Disconnect().ContinueWith(_ => { });
+            }
+            catch
+            {
+                // fire and forget
+            }
+            _currentName = null;
+        }
+
+        protected abstract Task SetCursorRectCore(PixelRect rect);
+        protected abstract Task SetActiveCore(bool active);
+        protected abstract Task ResetContextCore();
+        protected abstract Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode);
+
+        void UpdateActive()
+        {
+            _queue.Enqueue(async () =>
+            {
+                if(!IsConnected)
+                    return;
+                
+                var active = _windowActive && _controlActive;
+                if (active != _imeActive)
+                {
+                    _imeActive = active;
+                    await SetActiveCore(active);
+                }
+            });
+        }
+
+
+        void IX11InputMethodControl.SetWindowActive(bool active)
+        {
+            _windowActive = active;
+            UpdateActive();
+        }
+        
+        void ITextInputMethodImpl.SetActive(bool active)
+        {
+            _controlActive = active;
+            UpdateActive();
+        }
+
+        bool IX11InputMethodControl.IsEnabled => IsConnected && _imeActive == true;
+
+        async ValueTask<bool> IX11InputMethodControl.HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode)
+        {
+            try
+            {
+                return await _queue.EnqueueAsync(async () => await HandleKeyCore(args, keyVal, keyCode));
+            }
+            // Disconnected
+            catch (OperationCanceledException)
+            {
+                return false;
+            }
+            // Error, disconnect
+            catch (Exception e)
+            {
+                await QueueOnError(e);
+                return false;
+            }
+        }
+
+        private Action<string> _onCommit;
+        event Action<string> IX11InputMethodControl.Commit
+        {
+            add => _onCommit += value;
+            remove => _onCommit -= value;
+        }
+
+        protected void FireCommit(string s) => _onCommit?.Invoke(s);
+        
+        private Action<X11InputMethodForwardedKey> _onForward;
+        event Action<X11InputMethodForwardedKey> IX11InputMethodControl.ForwardKey
+        {
+            add => _onForward += value;
+            remove => _onForward -= value;
+        }
+
+        protected void FireForward(X11InputMethodForwardedKey k) => _onForward?.Invoke(k);
+        
+        void UpdateCursorRect()
+        {
+            _queue.Enqueue(async () =>
+            {
+                if(!IsConnected)
+                    return;
+                var cursorRect = PixelRect.FromRect(_logicalRect, _scaling);
+                cursorRect = cursorRect.Translate(_windowPosition);
+                if (cursorRect != _lastReportedRect)
+                {
+                    _lastReportedRect = cursorRect;
+                    await SetCursorRectCore(cursorRect);
+                }
+            });
+        }
+        
+        void IX11InputMethodControl.UpdateWindowInfo(PixelPoint position, double scaling)
+        {
+            _windowPosition = position;
+            _scaling = scaling;
+            UpdateCursorRect();
+        }
+
+        void ITextInputMethodImpl.SetCursorRect(Rect rect)
+        {
+            _logicalRect = rect;
+            UpdateCursorRect();
+        }
+
+        public abstract void SetOptions(TextInputOptionsQueryEventArgs options);
+
+        void ITextInputMethodImpl.Reset()
+        {
+            Reset();
+            _queue.Enqueue(async () =>
+            {
+                if (!IsConnected)
+                    return;
+                await ResetContextCore();
+            });
+        }
+    }
+}

+ 69 - 0
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs

@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Tmds.DBus;
+
+[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)]
+namespace Avalonia.FreeDesktop.DBusIme.Fcitx
+{
+    [DBusInterface("org.fcitx.Fcitx.InputMethod")]
+    interface IFcitxInputMethod : IDBusObject
+    {
+        Task<(int icid, bool enable, uint keyval1, uint state1, uint keyval2, uint state2)> CreateICv3Async(
+            string Appname, int Pid);
+    }
+    
+    
+    [DBusInterface("org.fcitx.Fcitx.InputContext")]
+    interface IFcitxInputContext : IDBusObject
+    {
+        Task EnableICAsync();
+        Task CloseICAsync();
+        Task FocusInAsync();
+        Task FocusOutAsync();
+        Task ResetAsync();
+        Task MouseEventAsync(int X);
+        Task SetCursorLocationAsync(int X, int Y);
+        Task SetCursorRectAsync(int X, int Y, int W, int H);
+        Task SetCapacityAsync(uint Caps);
+        Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor);
+        Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor);
+        Task DestroyICAsync();
+        Task<int> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time);
+        Task<IDisposable> WatchEnableIMAsync(Action handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchCloseIMAsync(Action handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchCommitStringAsync(Action<string> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception> onError = null);
+    }
+    
+    [DBusInterface("org.fcitx.Fcitx.InputContext1")]
+    interface IFcitxInputContext1 : IDBusObject
+    {
+        Task FocusInAsync();
+        Task FocusOutAsync();
+        Task ResetAsync();
+        Task SetCursorRectAsync(int X, int Y, int W, int H);
+        Task SetCapabilityAsync(ulong Caps);
+        Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor);
+        Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor);
+        Task DestroyICAsync();
+        Task<bool> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time);
+        Task<IDisposable> WatchCommitStringAsync(Action<string> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception> onError = null);
+    }
+
+    [DBusInterface("org.fcitx.Fcitx.InputMethod1")]
+    interface IFcitxInputMethod1 : IDBusObject
+    {
+        Task<(ObjectPath path, byte[] data)> CreateInputContextAsync((string, string)[] arg0);
+    }
+}

+ 67 - 0
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs

@@ -0,0 +1,67 @@
+using System;
+
+namespace Avalonia.FreeDesktop.DBusIme.Fcitx
+{
+    enum FcitxKeyEventType
+    {
+        FCITX_PRESS_KEY,
+        FCITX_RELEASE_KEY
+    };
+    
+    [Flags]
+    enum FcitxCapabilityFlags
+    {
+        CAPACITY_NONE = 0,
+        CAPACITY_CLIENT_SIDE_UI = (1 << 0),
+        CAPACITY_PREEDIT = (1 << 1),
+        CAPACITY_CLIENT_SIDE_CONTROL_STATE = (1 << 2),
+        CAPACITY_PASSWORD = (1 << 3),
+        CAPACITY_FORMATTED_PREEDIT = (1 << 4),
+        CAPACITY_CLIENT_UNFOCUS_COMMIT = (1 << 5),
+        CAPACITY_SURROUNDING_TEXT = (1 << 6),
+        CAPACITY_EMAIL = (1 << 7),
+        CAPACITY_DIGIT = (1 << 8),
+        CAPACITY_UPPERCASE = (1 << 9),
+        CAPACITY_LOWERCASE = (1 << 10),
+        CAPACITY_NOAUTOUPPERCASE = (1 << 11),
+        CAPACITY_URL = (1 << 12),
+        CAPACITY_DIALABLE = (1 << 13),
+        CAPACITY_NUMBER = (1 << 14),
+        CAPACITY_NO_ON_SCREEN_KEYBOARD = (1 << 15),
+        CAPACITY_SPELLCHECK = (1 << 16),
+        CAPACITY_NO_SPELLCHECK = (1 << 17),
+        CAPACITY_WORD_COMPLETION = (1 << 18),
+        CAPACITY_UPPERCASE_WORDS = (1 << 19),
+        CAPACITY_UPPERCASE_SENTENCES = (1 << 20),
+        CAPACITY_ALPHA = (1 << 21),
+        CAPACITY_NAME = (1 << 22),
+        CAPACITY_GET_IM_INFO_ON_FOCUS = (1 << 23),
+        CAPACITY_RELATIVE_CURSOR_RECT = (1 << 24),
+    };
+
+    [Flags]
+    enum FcitxKeyState
+    {
+        FcitxKeyState_None = 0,
+        FcitxKeyState_Shift = 1 << 0,
+        FcitxKeyState_CapsLock = 1 << 1,
+        FcitxKeyState_Ctrl = 1 << 2,
+        FcitxKeyState_Alt = 1 << 3,
+        FcitxKeyState_Alt_Shift = FcitxKeyState_Alt | FcitxKeyState_Shift,
+        FcitxKeyState_Ctrl_Shift = FcitxKeyState_Ctrl | FcitxKeyState_Shift,
+        FcitxKeyState_Ctrl_Alt = FcitxKeyState_Ctrl | FcitxKeyState_Alt,
+
+        FcitxKeyState_Ctrl_Alt_Shift =
+            FcitxKeyState_Ctrl | FcitxKeyState_Alt | FcitxKeyState_Shift,
+        FcitxKeyState_NumLock = 1 << 4,
+        FcitxKeyState_Super = 1 << 6,
+        FcitxKeyState_ScrollLock = 1 << 7,
+        FcitxKeyState_MousePressed = 1 << 8,
+        FcitxKeyState_HandledMask = 1 << 24,
+        FcitxKeyState_IgnoredMask = 1 << 25,
+        FcitxKeyState_Super2 = 1 << 26,
+        FcitxKeyState_Hyper = 1 << 27,
+        FcitxKeyState_Meta = 1 << 28,
+        FcitxKeyState_UsedMask = 0x5c001fff
+    };
+}

+ 51 - 0
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs

@@ -0,0 +1,51 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Avalonia.FreeDesktop.DBusIme.Fcitx
+{
+    internal class FcitxICWrapper
+    {
+        private readonly IFcitxInputContext1 _modern;
+        private readonly IFcitxInputContext _old;
+
+        public FcitxICWrapper(IFcitxInputContext old)
+        {
+            _old = old;
+        }
+
+        public FcitxICWrapper(IFcitxInputContext1 modern)
+        {
+            _modern = modern;
+        }
+
+        public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern.FocusInAsync();
+
+        public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern.FocusOutAsync();
+        
+        public Task ResetAsync() => _old?.ResetAsync() ?? _modern.ResetAsync();
+
+        public Task SetCursorRectAsync(int x, int y, int w, int h) =>
+            _old?.SetCursorRectAsync(x, y, w, h) ?? _modern.SetCursorRectAsync(x, y, w, h);
+        public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern.DestroyICAsync();
+
+        public async Task<bool> ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time)
+        {
+            if(_old!=null)
+                return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0;
+            return await _modern.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time);
+        }
+
+        public Task<IDisposable> WatchCommitStringAsync(Action<string> handler) =>
+            _old?.WatchCommitStringAsync(handler) ?? _modern.WatchCommitStringAsync(handler);
+
+        public Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler)
+        {
+            return _old?.WatchForwardKeyAsync(handler)
+                   ?? _modern.WatchForwardKeyAsync(ev =>
+                       handler((ev.keyval, ev.state, ev.type ? 1 : 0)));
+        }
+
+        public Task SetCapacityAsync(uint flags) =>
+            _old?.SetCapacityAsync(flags) ?? _modern.SetCapabilityAsync(flags);
+    }
+}

+ 149 - 0
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs

@@ -0,0 +1,149 @@
+using System;
+using System.Diagnostics;
+using System.Reactive.Concurrency;
+using System.Reflection;
+using System.Threading.Tasks;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+using Tmds.DBus;
+
+namespace Avalonia.FreeDesktop.DBusIme.Fcitx
+{
+    internal class FcitxX11TextInputMethod : DBusTextInputMethodBase
+    {
+        private FcitxICWrapper _context;
+        private FcitxCapabilityFlags? _lastReportedFlags;
+
+        public FcitxX11TextInputMethod(Connection connection) : base(connection,
+            "org.fcitx.Fcitx",
+            "org.freedesktop.portal.Fcitx"
+            )
+        {
+
+        }
+
+        protected override async Task<bool> Connect(string name)
+        {
+            if (name == "org.fcitx.Fcitx")
+            {
+                var method = Connection.CreateProxy<IFcitxInputMethod>(name, "/inputmethod");
+                var resp = await method.CreateICv3Async(GetAppName(),
+                    Process.GetCurrentProcess().Id);
+
+                var proxy = Connection.CreateProxy<IFcitxInputContext>(name,
+                    "/inputcontext_" + resp.icid);
+
+                _context = new FcitxICWrapper(proxy);
+            }
+            else
+            {
+                var method = Connection.CreateProxy<IFcitxInputMethod1>(name, "/inputmethod");
+                var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) });
+                var proxy = Connection.CreateProxy<IFcitxInputContext1>(name, resp.path);
+                _context = new FcitxICWrapper(proxy);
+            }
+
+            AddDisposable(await _context.WatchCommitStringAsync(OnCommitString));
+            AddDisposable(await _context.WatchForwardKeyAsync(OnForward));
+            return true;
+        }
+
+        protected override Task Disconnect() => _context.DestroyICAsync();
+
+        protected override void OnDisconnected() => _context = null;
+
+        protected override void Reset()
+        {
+            _lastReportedFlags = null;
+            base.Reset();
+        }
+
+        protected override Task SetCursorRectCore(PixelRect cursorRect) =>
+            _context.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width),
+                Math.Max(1, cursorRect.Height));
+
+        protected override Task SetActiveCore(bool active)
+        {
+            if (active)
+                return _context.FocusInAsync();
+            else
+                return _context.FocusOutAsync();
+        }
+
+        protected override Task ResetContextCore() => _context.ResetAsync();
+
+        protected override async Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode)
+        {
+            FcitxKeyState state = default;
+            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Control))
+                state |= FcitxKeyState.FcitxKeyState_Ctrl;
+            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Alt))
+                state |= FcitxKeyState.FcitxKeyState_Alt;
+            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Shift))
+                state |= FcitxKeyState.FcitxKeyState_Shift;
+            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Meta))
+                state |= FcitxKeyState.FcitxKeyState_Super;
+
+            var type = args.Type == RawKeyEventType.KeyDown ?
+                FcitxKeyEventType.FCITX_PRESS_KEY :
+                FcitxKeyEventType.FCITX_RELEASE_KEY;
+            
+            return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type,
+                (uint)args.Timestamp).ConfigureAwait(false);
+        }
+        
+        public override void SetOptions(TextInputOptionsQueryEventArgs options) =>
+            Enqueue(async () =>
+            {
+                if(_context == null)
+                    return;
+                FcitxCapabilityFlags flags = default;
+                if (options.Lowercase)
+                    flags |= FcitxCapabilityFlags.CAPACITY_LOWERCASE;
+                if (options.Uppercase)
+                    flags |= FcitxCapabilityFlags.CAPACITY_UPPERCASE;
+                if (!options.AutoCapitalization)
+                    flags |= FcitxCapabilityFlags.CAPACITY_NOAUTOUPPERCASE;
+                if (options.ContentType == TextInputContentType.Email)
+                    flags |= FcitxCapabilityFlags.CAPACITY_EMAIL;
+                else if (options.ContentType == TextInputContentType.Number)
+                    flags |= FcitxCapabilityFlags.CAPACITY_NUMBER;
+                else if (options.ContentType == TextInputContentType.Password)
+                    flags |= FcitxCapabilityFlags.CAPACITY_PASSWORD;
+                else if (options.ContentType == TextInputContentType.Phone)
+                    flags |= FcitxCapabilityFlags.CAPACITY_DIALABLE;
+                else if (options.ContentType == TextInputContentType.Url)
+                    flags |= FcitxCapabilityFlags.CAPACITY_URL;
+                if (flags != _lastReportedFlags)
+                {
+                    _lastReportedFlags = flags;
+                    await _context.SetCapacityAsync((uint)flags);
+                }
+            });
+
+        private void OnForward((uint keyval, uint state, int type) ev)
+        {
+            var state = (FcitxKeyState)ev.state;
+            KeyModifiers mods = default;
+            if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Ctrl))
+                mods |= KeyModifiers.Control;
+            if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Alt))
+                mods |= KeyModifiers.Alt;
+            if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Shift))
+                mods |= KeyModifiers.Shift;
+            if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Super))
+                mods |= KeyModifiers.Meta;
+            FireForward(new X11InputMethodForwardedKey
+            {
+                Modifiers = mods,
+                KeyVal = (int)ev.keyval,
+                Type = ev.type == (int)FcitxKeyEventType.FCITX_PRESS_KEY ?
+                    RawKeyEventType.KeyDown :
+                    RawKeyEventType.KeyUp
+            });
+        }
+
+        private void OnCommitString(string s) => FireCommit(s);
+    }
+}

+ 52 - 0
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs

@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Tmds.DBus;
+
+[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
+namespace Avalonia.FreeDesktop.DBusIme.IBus
+{
+    [DBusInterface("org.freedesktop.IBus.InputContext")]
+    interface IIBusInputContext : IDBusObject
+    {
+        Task<bool> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State);
+        Task SetCursorLocationAsync(int X, int Y, int W, int H);
+        Task FocusInAsync();
+        Task FocusOutAsync();
+        Task ResetAsync();
+        Task SetCapabilitiesAsync(uint Caps);
+        Task PropertyActivateAsync(string Name, int State);
+        Task SetEngineAsync(string Name);
+        Task<object> GetEngineAsync();
+        Task DestroyAsync();
+        Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos);
+        Task<IDisposable> WatchCommitTextAsync(Action<object> cb, Action<Exception> onError = null);
+        Task<IDisposable> WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchRequireSurroundingTextAsync(Action handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchShowPreeditTextAsync(Action handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchHidePreeditTextAsync(Action handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchShowAuxiliaryTextAsync(Action handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchHideAuxiliaryTextAsync(Action handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchShowLookupTableAsync(Action handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchHideLookupTableAsync(Action handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchPageUpLookupTableAsync(Action handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchPageDownLookupTableAsync(Action handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchCursorUpLookupTableAsync(Action handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchCursorDownLookupTableAsync(Action handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchRegisterPropertiesAsync(Action<object> handler, Action<Exception> onError = null);
+        Task<IDisposable> WatchUpdatePropertyAsync(Action<object> handler, Action<Exception> onError = null);
+    }
+    
+    
+    [DBusInterface("org.freedesktop.IBus.Portal")]
+    interface IIBusPortal : IDBusObject
+    {
+        Task<ObjectPath> CreateInputContextAsync(string Name);
+    }
+}

+ 45 - 0
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs

@@ -0,0 +1,45 @@
+using System;
+
+namespace Avalonia.FreeDesktop.DBusIme.IBus
+{
+    [Flags]
+    internal enum IBusModifierMask
+    {
+        ShiftMask    = 1 << 0,
+        LockMask     = 1 << 1,
+        ControlMask  = 1 << 2,
+        Mod1Mask     = 1 << 3,
+        Mod2Mask     = 1 << 4,
+        Mod3Mask     = 1 << 5,
+        Mod4Mask     = 1 << 6,
+        Mod5Mask     = 1 << 7,
+        Button1Mask  = 1 << 8,
+        Button2Mask  = 1 << 9,
+        Button3Mask  = 1 << 10,
+        Button4Mask  = 1 << 11,
+        Button5Mask  = 1 << 12,
+        
+        HandledMask  = 1 << 24,
+        ForwardMask  = 1 << 25,
+        IgnoredMask  = ForwardMask,
+
+        SuperMask    = 1 << 26,
+        HyperMask    = 1 << 27,
+        MetaMask     = 1 << 28,
+
+        ReleaseMask  = 1 << 30,
+
+        ModifierMask = 0x5c001fff
+    }
+
+    [Flags]
+    internal enum IBusCapability
+    {
+        CapPreeditText = 1 << 0,
+        CapAuxiliaryText = 1 << 1,
+        CapLookupTable = 1 << 2,
+        CapFocus = 1 << 3,
+        CapProperty = 1 << 4,
+        CapSurroundingText = 1 << 5,
+    }
+}

+ 105 - 0
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs

@@ -0,0 +1,105 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+using Tmds.DBus;
+
+namespace Avalonia.FreeDesktop.DBusIme.IBus
+{
+    internal class IBusX11TextInputMethod : DBusTextInputMethodBase
+    {
+        private IIBusInputContext _context;
+
+        public IBusX11TextInputMethod(Connection connection) : base(connection, 
+            "org.freedesktop.portal.IBus")
+        {
+        }
+
+        protected override async Task<bool> Connect(string name)
+        {
+            var path =
+                await Connection.CreateProxy<IIBusPortal>(name, "/org/freedesktop/IBus")
+                    .CreateInputContextAsync(GetAppName());
+
+            _context = Connection.CreateProxy<IIBusInputContext>(name, path);
+            AddDisposable(await _context.WatchCommitTextAsync(OnCommitText));
+            AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey));
+            Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus));
+            return true;
+        }
+
+        private void OnForwardKey((uint keyval, uint keycode, uint state) k)
+        {
+            var state = (IBusModifierMask)k.state;
+            KeyModifiers mods = default;
+            if (state.HasFlagCustom(IBusModifierMask.ControlMask))
+                mods |= KeyModifiers.Control;
+            if (state.HasFlagCustom(IBusModifierMask.Mod1Mask))
+                mods |= KeyModifiers.Alt;
+            if (state.HasFlagCustom(IBusModifierMask.ShiftMask))
+                mods |= KeyModifiers.Shift;
+            if (state.HasFlagCustom(IBusModifierMask.Mod4Mask))
+                mods |= KeyModifiers.Meta;
+            FireForward(new X11InputMethodForwardedKey
+            {
+                KeyVal = (int)k.keyval,
+                Type = state.HasFlagCustom(IBusModifierMask.ReleaseMask) ? RawKeyEventType.KeyUp : RawKeyEventType.KeyDown,
+                Modifiers = mods
+            });
+        }
+
+        
+        private void OnCommitText(object wtf)
+        {
+            // Hello darkness, my old friend
+            var prop = wtf.GetType().GetField("Item3");
+            if (prop != null)
+            {
+                var text = (string)prop.GetValue(wtf);
+                if (!string.IsNullOrEmpty(text))
+                    FireCommit(text);
+            }
+        }
+
+        protected override Task Disconnect() => _context.DestroyAsync();
+
+        protected override void OnDisconnected()
+        {
+            _context = null;
+            base.OnDisconnected();
+        }
+
+        protected override Task SetCursorRectCore(PixelRect rect) 
+            => _context.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height);
+
+        protected override Task SetActiveCore(bool active)
+            => active ? _context.FocusInAsync() : _context.FocusOutAsync();
+
+        protected override Task ResetContextCore()
+            => _context.ResetAsync();
+
+        protected override Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode)
+        {
+            IBusModifierMask state = default;
+            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Control))
+                state |= IBusModifierMask.ControlMask;
+            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Alt))
+                state |= IBusModifierMask.Mod1Mask;
+            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Shift))
+                state |= IBusModifierMask.ShiftMask;
+            if (args.Modifiers.HasFlagCustom(RawInputModifiers.Meta))
+                state |= IBusModifierMask.Mod4Mask;
+
+            if (args.Type == RawKeyEventType.KeyUp)
+                state |= IBusModifierMask.ReleaseMask;
+
+            return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state);
+        }
+
+        public override void SetOptions(TextInputOptionsQueryEventArgs options)
+        {
+            // No-op, because ibus 
+        }
+    }
+}

+ 53 - 0
src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs

@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.FreeDesktop.DBusIme.Fcitx;
+using Avalonia.FreeDesktop.DBusIme.IBus;
+using Tmds.DBus;
+
+namespace Avalonia.FreeDesktop.DBusIme
+{
+    public class X11DBusImeHelper
+    {
+        private static readonly Dictionary<string, Func<Connection, IX11InputMethodFactory>> KnownMethods =
+            new Dictionary<string, Func<Connection, IX11InputMethodFactory>>
+            {
+                ["fcitx"] = conn =>
+                    new DBusInputMethodFactory<FcitxX11TextInputMethod>(_ => new FcitxX11TextInputMethod(conn)),
+                ["ibus"] = conn =>
+                    new DBusInputMethodFactory<IBusX11TextInputMethod>(_ => new IBusX11TextInputMethod(conn))
+            };
+        
+        static Func<Connection, IX11InputMethodFactory> DetectInputMethod()
+        {
+            foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" })
+            {
+                var value = Environment.GetEnvironmentVariable(name);
+                
+                if (value == "none")
+                    return null;
+                
+                if (value != null && KnownMethods.TryGetValue(value, out var factory))
+                    return factory;
+            }
+
+            return null;
+        }
+        
+        public static bool DetectAndRegister()
+        {
+            var factory = DetectInputMethod();
+            if (factory != null)
+            {
+                var conn = DBusHelper.TryInitialize();
+                if (conn != null)
+                {
+                    AvaloniaLocator.CurrentMutable.Bind<IX11InputMethodFactory>().ToConstant(factory(conn));
+                    return true;
+                }
+            }
+
+            return false;
+
+        }
+    }
+}

+ 31 - 0
src/Avalonia.FreeDesktop/IX11InputMethod.cs

@@ -0,0 +1,31 @@
+using System;
+using System.Threading.Tasks;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+
+namespace Avalonia.FreeDesktop
+{
+    public interface IX11InputMethodFactory
+    {
+        (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid);
+    }
+
+    public struct X11InputMethodForwardedKey
+    {
+        public int KeyVal { get; set; }
+        public KeyModifiers Modifiers { get; set; }
+        public RawKeyEventType Type { get; set; }
+    }
+    
+    public interface IX11InputMethodControl : IDisposable
+    {
+        void SetWindowActive(bool active);
+        bool IsEnabled { get; }
+        ValueTask<bool> HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode);
+        event Action<string> Commit;
+        event Action<X11InputMethodForwardedKey> ForwardKey;
+        
+        void UpdateWindowInfo(PixelPoint position, double scaling);
+    }
+}

+ 35 - 0
src/Avalonia.Input/InputElement.cs

@@ -5,6 +5,7 @@ using Avalonia.Controls;
 using Avalonia.Controls.Metadata;
 using Avalonia.Data;
 using Avalonia.Input.GestureRecognizers;
+using Avalonia.Input.TextInput;
 using Avalonia.Interactivity;
 using Avalonia.VisualTree;
 
@@ -103,6 +104,22 @@ namespace Avalonia.Input
             RoutedEvent.Register<InputElement, TextInputEventArgs>(
                 "TextInput",
                 RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+        
+        /// <summary>
+        /// Defines the <see cref="TextInputMethodClientRequested"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<TextInputMethodClientRequestedEventArgs> TextInputMethodClientRequestedEvent =
+            RoutedEvent.Register<InputElement, TextInputMethodClientRequestedEventArgs>(
+                "TextInputMethodClientRequested",
+                RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+        
+        /// <summary>
+        /// Defines the <see cref="TextInputOptionsQuery"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<TextInputOptionsQueryEventArgs> TextInputOptionsQueryEvent =
+            RoutedEvent.Register<InputElement, TextInputOptionsQueryEventArgs>(
+                "TextInputOptionsQuery",
+                RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
 
         /// <summary>
         /// Defines the <see cref="PointerEnter"/> event.
@@ -243,6 +260,24 @@ namespace Avalonia.Input
             add { AddHandler(TextInputEvent, value); }
             remove { RemoveHandler(TextInputEvent, value); }
         }
+        
+        /// <summary>
+        /// Occurs when an input element gains input focus and input method is looking for the corresponding client
+        /// </summary>
+        public event EventHandler<TextInputMethodClientRequestedEventArgs> TextInputMethodClientRequested
+        {
+            add { AddHandler(TextInputMethodClientRequestedEvent, value); }
+            remove { RemoveHandler(TextInputMethodClientRequestedEvent, value); }
+        }
+        
+        /// <summary>
+        /// Occurs when an input element gains input focus and input method is asking for required content options
+        /// </summary>
+        public event EventHandler<TextInputOptionsQueryEventArgs> TextInputOptionsQuery
+        {
+            add { AddHandler(TextInputOptionsQueryEvent, value); }
+            remove { RemoveHandler(TextInputOptionsQueryEvent, value); }
+        }
 
         /// <summary>
         /// Occurs when the pointer enters the control.

+ 6 - 0
src/Avalonia.Input/KeyboardDevice.cs

@@ -1,6 +1,7 @@
 using System.ComponentModel;
 using System.Runtime.CompilerServices;
 using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
 using Avalonia.Interactivity;
 using Avalonia.VisualTree;
 
@@ -18,6 +19,10 @@ namespace Avalonia.Input
         public IInputManager InputManager => AvaloniaLocator.Current.GetService<IInputManager>();
 
         public IFocusManager FocusManager => AvaloniaLocator.Current.GetService<IFocusManager>();
+        
+        // This should live in the FocusManager, but with the current outdated architecture
+        // the source of truth about the input focus is in KeyboardDevice
+        private readonly TextInputMethodManager _textInputManager = new TextInputMethodManager();
 
         public IInputElement? FocusedElement
         {
@@ -40,6 +45,7 @@ namespace Avalonia.Input
                 }
                 
                 RaisePropertyChanged();
+                _textInputManager.SetFocusedElement(value);
             }
         }
 

+ 60 - 0
src/Avalonia.Input/TextInput/ITextInputMethodClient.cs

@@ -0,0 +1,60 @@
+using System;
+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>
+        IVisual 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-commited input on the cursor position
+        /// </summary>
+        bool SupportsPreedit { get; }
+        /// <summary>
+        /// Sets the non-commited input string
+        /// </summary>
+        void SetPreeditText(string text);
+        /// <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>
+        /// Returns the text before the cursor. Must return a non-empty string if cursor is not at the beginning of the text entry
+        /// </summary>
+        string TextBeforeCursor { get; }
+        /// <summary>
+        /// Returns the text before the cursor. Must return a non-empty string if cursor is not at the end of the text entry
+        /// </summary>
+        string TextAfterCursor { get; }
+    }
+
+    public struct TextInputMethodSurroundingText
+    {
+        public string Text { get; set; }
+        public int CursorOffset { get; set; }
+        public int AnchorOffset { get; set; }
+    }
+}

+ 15 - 0
src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs

@@ -0,0 +1,15 @@
+namespace Avalonia.Input.TextInput
+{
+    public interface ITextInputMethodImpl
+    {
+        void SetActive(bool active);
+        void SetCursorRect(Rect rect);
+        void SetOptions(TextInputOptionsQueryEventArgs options);
+        void Reset();
+    }
+    
+    public interface ITextInputMethodRoot : IInputRoot
+    {
+        ITextInputMethodImpl InputMethod { get; }
+    }
+}

+ 101 - 0
src/Avalonia.Input/TextInput/InputMethodManager.cs

@@ -0,0 +1,101 @@
+using System;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Input.TextInput
+{
+    internal class TextInputMethodManager
+    {
+        private ITextInputMethodImpl? _im;
+        private IInputElement? _focusedElement;
+        private ITextInputMethodClient? _client;
+        private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper();
+
+        public TextInputMethodManager() => _transformTracker.MatrixChanged += UpdateCursorRect;
+
+        private ITextInputMethodClient? Client
+        {
+            get => _client;
+            set
+            {
+                if(_client == value)
+                    return;
+                if (_client != null)
+                {
+                    _client.CursorRectangleChanged -= OnCursorRectangleChanged;
+                    _client.TextViewVisualChanged -= OnTextViewVisualChanged;
+                }
+
+                _client = value;
+                
+                if (_client != null)
+                {
+                    _client.CursorRectangleChanged += OnCursorRectangleChanged;
+                    _client.TextViewVisualChanged += OnTextViewVisualChanged;
+                    var optionsQuery = new TextInputOptionsQueryEventArgs
+                    {
+                        RoutedEvent = InputElement.TextInputOptionsQueryEvent
+                    };
+                    _focusedElement?.RaiseEvent(optionsQuery);
+                    _im?.Reset();
+                    _im?.SetOptions(optionsQuery);
+                    _transformTracker?.SetVisual(_client?.TextViewVisual);
+                    UpdateCursorRect();
+                    _im?.SetActive(true);
+                }
+                else
+                {
+                    _im?.SetActive(false);
+                    _transformTracker.SetVisual(null);
+                }
+            }
+        }
+
+        private void OnTextViewVisualChanged(object sender, EventArgs e) 
+            => _transformTracker.SetVisual(_client?.TextViewVisual);
+
+        private void UpdateCursorRect()
+        {
+            if (_im == null || _client == null || _focusedElement?.VisualRoot == null)
+                return;
+            var transform = _focusedElement.TransformToVisual(_focusedElement.VisualRoot);
+            if (transform == null)
+                _im.SetCursorRect(default);
+            else
+                _im.SetCursorRect(_client.CursorRectangle.TransformToAABB(transform.Value));
+        }
+
+        private void OnCursorRectangleChanged(object sender, EventArgs e)
+        {
+            if (sender == _client)
+                UpdateCursorRect();
+        }
+        
+        public void SetFocusedElement(IInputElement? element)
+        {
+            if(_focusedElement == element)
+                return;
+            _focusedElement = element;
+            
+            var inputMethod = (element?.VisualRoot as ITextInputMethodRoot)?.InputMethod;
+            if(_im != inputMethod)
+                _im?.SetActive(false);
+
+            _im = inputMethod;
+            
+            if (_focusedElement == null || _im == null)
+            {
+                Client = null;
+                _im?.SetActive(false);
+                return;
+            }
+
+            var clientQuery = new TextInputMethodClientRequestedEventArgs
+            {
+                RoutedEvent = InputElement.TextInputMethodClientRequestedEvent
+            };
+            
+            _focusedElement.RaiseEvent(clientQuery);
+            Client = clientQuery.Client;
+        }
+    }
+}

+ 12 - 0
src/Avalonia.Input/TextInput/TextInputContentType.cs

@@ -0,0 +1,12 @@
+namespace Avalonia.Input.TextInput
+{
+    public enum TextInputContentType
+    {
+        Normal = 0,
+        Email = 1,
+        Phone = 2,
+        Number = 3,
+        Url = 4,
+        Password = 5
+    }
+}

+ 12 - 0
src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs

@@ -0,0 +1,12 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Input.TextInput
+{
+    public class TextInputMethodClientRequestedEventArgs : RoutedEventArgs
+    {
+        /// <summary>
+        /// Set this property to a valid text input client to enable input method interaction
+        /// </summary>
+        public ITextInputMethodClient? Client { get; set; }
+    }
+}

+ 32 - 0
src/Avalonia.Input/TextInput/TextInputOptionsQueryEventArgs.cs

@@ -0,0 +1,32 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Input.TextInput
+{
+    public class TextInputOptionsQueryEventArgs : RoutedEventArgs
+    {
+        /// <summary>
+        /// The content type (mostly for determining the shape of the virtual keyboard)
+        /// </summary>
+        public TextInputContentType ContentType { get; set; }
+        /// <summary>
+        /// Text is multiline
+        /// </summary>
+        public bool Multiline { get; set; }
+        /// <summary>
+        /// Text is in lower case
+        /// </summary>
+        public bool Lowercase { get; set; }
+        /// <summary>
+        /// Text is in upper case
+        /// </summary>
+        public bool Uppercase { get; set; }
+        /// <summary>
+        /// Automatically capitalize letters at the start of the sentence
+        /// </summary>
+        public bool AutoCapitalization { get; set; }
+        /// <summary>
+        /// Text contains sensitive data like card numbers and should not be stored  
+        /// </summary>
+        public bool IsSensitive { get; set; }
+    }
+}

+ 109 - 0
src/Avalonia.Input/TextInput/TransformTrackingHelper.cs

@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Input.TextInput
+{
+    class TransformTrackingHelper : IDisposable
+    {
+        private IVisual? _visual;
+        private bool _queuedForUpdate;
+        private readonly EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChangedHandler;
+        private readonly List<Visual> _propertyChangedSubscriptions = new List<Visual>();
+        
+        public TransformTrackingHelper()
+        {
+            _propertyChangedHandler = PropertyChangedHandler;
+        }
+
+        public void SetVisual(IVisual? visual)
+        {
+            Dispose();
+            _visual = visual;
+            if (visual != null)
+            {
+                visual.AttachedToVisualTree += OnAttachedToVisualTree;
+                visual.DetachedFromVisualTree -= OnDetachedFromVisualTree;
+                if (visual.IsAttachedToVisualTree)
+                    SubscribeToParents();
+                UpdateMatrix();
+            }
+        }
+        
+        public Matrix? Matrix { get; private set; }
+        public event Action? MatrixChanged;
+        
+        public void Dispose()
+        {
+            if(_visual == null)
+                return;
+            UnsubscribeFromParents();
+            _visual.AttachedToVisualTree -= OnAttachedToVisualTree;
+            _visual.DetachedFromVisualTree -= OnDetachedFromVisualTree;
+            _visual = null;
+        }
+
+        private void SubscribeToParents()
+        {
+            var visual = _visual;
+            // ReSharper disable once ConditionIsAlwaysTrueOrFalse
+            // false positive
+            while (visual != null)
+            {
+                if (visual is Visual v)
+                {
+                    v.PropertyChanged += _propertyChangedHandler;
+                    _propertyChangedSubscriptions.Add(v);
+                }
+
+                visual = visual.VisualParent;
+            }
+        }
+
+        private void UnsubscribeFromParents()
+        {
+            foreach (var v in _propertyChangedSubscriptions)
+                v.PropertyChanged -= _propertyChangedHandler;
+            _propertyChangedSubscriptions.Clear();
+        }
+
+        void UpdateMatrix()
+        {
+            Matrix? matrix = null;
+            if (_visual != null && _visual.VisualRoot != null)
+                matrix = _visual.TransformToVisual(_visual.VisualRoot);
+            if (Matrix != matrix)
+            {
+                Matrix = matrix;
+                MatrixChanged?.Invoke();
+            }
+        }
+
+        private void OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs visualTreeAttachmentEventArgs)
+        {
+            SubscribeToParents();
+            UpdateMatrix();
+        }
+
+        private void EnqueueForUpdate()
+        {
+            if(_queuedForUpdate)
+                return;
+            _queuedForUpdate = true;
+            Dispatcher.UIThread.Post(UpdateMatrix, DispatcherPriority.Render);
+        }
+
+        private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.IsEffectiveValueChange && e.Property == Visual.BoundsProperty)
+                EnqueueForUpdate();
+        }
+
+        private void OnDetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs visualTreeAttachmentEventArgs)
+        {
+            UnsubscribeFromParents();
+            UpdateMatrix();
+        }
+    }
+}

+ 2 - 2
src/Avalonia.X11/X11Clipboard.cs

@@ -52,7 +52,7 @@ namespace Avalonia.X11
                         : null;
         }
         
-        private unsafe void OnEvent(XEvent ev)
+        private unsafe void OnEvent(ref XEvent ev)
         {
             if (ev.type == XEventName.SelectionRequest)
             {       
@@ -62,7 +62,7 @@ namespace Avalonia.X11
                     SelectionEvent =
                     {
                         type = XEventName.SelectionNotify,
-                        send_event = true,
+                        send_event = 1,
                         display = _x11.Display,
                         selection = sel.selection,
                         target = sel.target,

+ 2 - 2
src/Avalonia.X11/X11Globals.cs

@@ -123,7 +123,7 @@ namespace Avalonia.X11
             }
         }
 
-        private void HandleCompositionAtomOwnerEvents(XEvent ev)
+        private void HandleCompositionAtomOwnerEvents(ref XEvent ev)
         {
             if(ev.type == XEventName.DestroyNotify)
                 UpdateCompositingAtomOwner();
@@ -154,7 +154,7 @@ namespace Avalonia.X11
             }
         }
         
-        private void OnRootWindowEvent(XEvent ev)
+        private void OnRootWindowEvent(ref XEvent ev)
         {
             if (ev.type == XEventName.PropertyNotify)
             {

+ 21 - 4
src/Avalonia.X11/X11Info.cs

@@ -32,8 +32,10 @@ namespace Avalonia.X11
 
         public IntPtr LastActivityTimestamp { get; set; }
         public XVisualInfo? TransparentVisualInfo { get; set; }
+        public bool HasXim { get; set; }
+        public IntPtr DefaultFontSet { get; set; }
         
-        public unsafe X11Info(IntPtr display, IntPtr deferredDisplay)
+        public unsafe X11Info(IntPtr display, IntPtr deferredDisplay, bool useXim)
         {
             Display = display;
             DeferredDisplay = deferredDisplay;
@@ -43,9 +45,24 @@ namespace Avalonia.X11
             DefaultCursor = XCreateFontCursor(display, CursorFontShape.XC_top_left_arrow);
             DefaultRootWindow = XDefaultRootWindow(display);
             Atoms = new X11Atoms(display);
-            //TODO: Open an actual XIM once we get support for preedit in our textbox
-            XSetLocaleModifiers("@im=none");
-            Xim = XOpenIM(display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
+
+            DefaultFontSet = XCreateFontSet(Display, "-*-*-*-*-*-*-*-*-*-*-*-*-*-*",
+                out var _, out var _, IntPtr.Zero);
+            
+            if (useXim)
+            {
+                XSetLocaleModifiers("");
+                Xim = XOpenIM(display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
+                if (Xim != IntPtr.Zero)
+                    HasXim = true;
+            }
+
+            if (Xim == IntPtr.Zero)
+            {
+                XSetLocaleModifiers("@im=none");
+                Xim = XOpenIM(display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
+            }
+
             XMatchVisualInfo(Display, DefaultScreen, 32, 4, out var visual);
             if (visual.depth == 32)
                 TransparentVisualInfo = visual;

+ 70 - 2
src/Avalonia.X11/X11Platform.cs

@@ -1,9 +1,11 @@
 using System;
 using System.Collections.Generic;
 using System.Reflection;
+using System.Runtime.InteropServices;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
 using Avalonia.FreeDesktop;
+using Avalonia.FreeDesktop.DBusIme;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
 using Avalonia.OpenGL;
@@ -21,7 +23,8 @@ namespace Avalonia.X11
     {
         private Lazy<KeyboardDevice> _keyboardDevice = new Lazy<KeyboardDevice>(() => new KeyboardDevice());
         public KeyboardDevice KeyboardDevice => _keyboardDevice.Value;
-        public Dictionary<IntPtr, Action<XEvent>> Windows = new Dictionary<IntPtr, Action<XEvent>>();
+        public Dictionary<IntPtr, X11PlatformThreading.EventHandler> Windows =
+            new Dictionary<IntPtr, X11PlatformThreading.EventHandler>();
         public XI2Manager XI2;
         public X11Info Info { get; private set; }
         public IX11Screens X11Screens { get; private set; }
@@ -29,9 +32,24 @@ namespace Avalonia.X11
         public X11PlatformOptions Options { get; private set; }
         public IntPtr OrphanedWindow { get; private set; }
         public X11Globals Globals { get; private set; }
+        [DllImport("libc")]
+        static extern void setlocale(int type, string s);
         public void Initialize(X11PlatformOptions options)
         {
             Options = options;
+            
+            bool useXim = false;
+            if (EnableIme(options))
+            {
+                // Attempt to configure DBus-based input method and check if we can fall back to XIM
+                if (!X11DBusImeHelper.DetectAndRegister() && ShouldUseXim())
+                    useXim = true;
+            }
+
+            // XIM doesn't work at all otherwise
+            if (useXim)
+                setlocale(0, "");
+
             XInitThreads();
             Display = XOpenDisplay(IntPtr.Zero);
             DeferredDisplay = XOpenDisplay(IntPtr.Zero);
@@ -40,7 +58,8 @@ namespace Avalonia.X11
             if (Display == IntPtr.Zero)
                 throw new Exception("XOpenDisplay failed");
             XError.Init();
-            Info = new X11Info(Display, DeferredDisplay);
+            
+            Info = new X11Info(Display, DeferredDisplay, useXim);
             Globals = new X11Globals(this);
             //TODO: log
             if (options.UseDBusMenu)
@@ -90,6 +109,54 @@ namespace Avalonia.X11
         {
             throw new NotSupportedException();
         }
+
+        bool EnableIme(X11PlatformOptions options)
+        {
+            // Disable if explicitly asked by user
+            var avaloniaImModule = Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE");
+            if (avaloniaImModule == "none")
+                return false;
+            
+            // Use value from options when specified
+            if (options.EnableIme.HasValue)
+                return options.EnableIme.Value;
+            
+            // Automatically enable for CJK locales
+            var lang = Environment.GetEnvironmentVariable("LANG");
+            var isCjkLocale = lang != null &&
+                              (lang.Contains("zh")
+                               || lang.Contains("ja")
+                               || lang.Contains("vi")
+                               || lang.Contains("ko"));
+
+            return isCjkLocale;
+        }
+        
+        bool ShouldUseXim()
+        {
+            // Check if we are forbidden from using IME
+            if (Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE") == "none"
+                || Environment.GetEnvironmentVariable("GTK_IM_MODULE") == "none"
+                || Environment.GetEnvironmentVariable("QT_IM_MODULE") == "none")
+                return true;
+            
+            // Check if XIM is configured
+            var modifiers = Environment.GetEnvironmentVariable("XMODIFIERS");
+            if (modifiers == null)
+                return false;
+            if (modifiers.Contains("@im=none") || modifiers.Contains("@im=null"))
+                return false;
+            if (!modifiers.Contains("@im="))
+                return false;
+            
+            // Check if we are configured to use it
+            if (Environment.GetEnvironmentVariable("GTK_IM_MODULE") == "xim"
+                || Environment.GetEnvironmentVariable("QT_IM_MODULE") == "xim"
+                || Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE") == "xim")
+                return true;
+            
+            return false;
+        }
     }
 }
 
@@ -103,6 +170,7 @@ namespace Avalonia
         public bool OverlayPopups { get; set; }
         public bool UseDBusMenu { get; set; }
         public bool UseDeferredRendering { get; set; } = true;
+        public bool? EnableIme { get; set; }
 
         public IList<GlVersion> GlProfiles { get; set; } = new List<GlVersion>
         {

+ 8 - 3
src/Avalonia.X11/X11PlatformThreading.cs

@@ -13,7 +13,9 @@ namespace Avalonia.X11
     {
         private readonly AvaloniaX11Platform _platform;
         private readonly IntPtr _display;
-        private readonly Dictionary<IntPtr, Action<XEvent>> _eventHandlers;
+
+        public delegate void EventHandler(ref XEvent xev);
+        private readonly Dictionary<IntPtr, EventHandler> _eventHandlers;
         private Thread _mainThread;
 
         [StructLayout(LayoutKind.Explicit)]
@@ -162,13 +164,16 @@ namespace Avalonia.X11
             Signaled?.Invoke(prio);
         }
 
-        void HandleX11(CancellationToken cancellationToken)
+        unsafe void HandleX11(CancellationToken cancellationToken)
         {
             while (XPending(_display) != 0)
             {
                 if (cancellationToken.IsCancellationRequested)
                     return;
                 XNextEvent(_display, out var xev);
+                if(XFilterEvent(ref xev, IntPtr.Zero))
+                    continue;
+
                 if (xev.type == XEventName.GenericEvent)
                     XGetEventData(_display, &xev.GenericEventCookie);
                 try
@@ -182,7 +187,7 @@ namespace Avalonia.X11
                         }
                     }
                     else if (_eventHandlers.TryGetValue(xev.AnyEvent.window, out var handler))
-                        handler(xev);
+                        handler(ref xev);
                 }
                 finally
                 {

+ 1 - 1
src/Avalonia.X11/X11Screens.cs

@@ -75,7 +75,7 @@ namespace Avalonia.X11
                 XRRSelectInput(_x11.Display, _window, RandrEventMask.RRScreenChangeNotify);
             }
 
-            private void OnEvent(XEvent ev)
+            private void OnEvent(ref XEvent ev)
             {
                 // Invalidate cache on RRScreenChangeNotify
                 if ((int)ev.type == _x11.RandrEventBase + (int)RandrEvent.RRScreenChangeNotify)

+ 76 - 54
src/Avalonia.X11/X11Structs.cs

@@ -53,7 +53,7 @@ namespace Avalonia.X11 {
 	internal struct XAnyEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		window;
 	}
@@ -62,7 +62,7 @@ namespace Avalonia.X11 {
 	internal struct XKeyEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		window;
 		internal IntPtr		root;
@@ -74,14 +74,14 @@ namespace Avalonia.X11 {
 		internal int		y_root;
 	    internal XModifierMask state;
 		internal int		keycode;
-		internal bool		same_screen;
+		internal int		same_screen;
 	}
 
 	[StructLayout(LayoutKind.Sequential)]
 	internal struct XButtonEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		window;
 		internal IntPtr		root;
@@ -93,14 +93,14 @@ namespace Avalonia.X11 {
 		internal int		y_root;
 		internal XModifierMask		state;
 		internal int		button;
-		internal bool		same_screen;
+		internal int		same_screen;
 	}
 
 	[StructLayout(LayoutKind.Sequential)]
 	internal struct XMotionEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		window;
 		internal IntPtr		root;
@@ -112,14 +112,14 @@ namespace Avalonia.X11 {
 		internal int		y_root;
 		internal XModifierMask		state;
 		internal byte		is_hint;
-		internal bool		same_screen;
+		internal int		same_screen;
 	}
 
 	[StructLayout(LayoutKind.Sequential)]
 	internal struct XCrossingEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		window;
 		internal IntPtr		root;
@@ -131,8 +131,8 @@ namespace Avalonia.X11 {
 		internal int		y_root;
 		internal NotifyMode	mode;
 		internal NotifyDetail	detail;
-		internal bool		same_screen;
-		internal bool		focus;
+		internal int		same_screen;
+		internal int		focus;
 		internal XModifierMask		state;
 	}
 
@@ -140,7 +140,7 @@ namespace Avalonia.X11 {
 	internal struct XFocusChangeEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		window;
 		internal int		mode;
@@ -151,7 +151,7 @@ namespace Avalonia.X11 {
 	internal struct XKeymapEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		window;
 		internal byte		key_vector0;
@@ -192,7 +192,7 @@ namespace Avalonia.X11 {
 	internal struct XExposeEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		window;
 		internal int		x;
@@ -206,7 +206,7 @@ namespace Avalonia.X11 {
 	internal struct XGraphicsExposeEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		drawable;
 		internal int		x;
@@ -222,7 +222,7 @@ namespace Avalonia.X11 {
 	internal struct XNoExposeEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		drawable;
 		internal int		major_code;
@@ -233,7 +233,7 @@ namespace Avalonia.X11 {
 	internal struct XVisibilityEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		window;
 		internal int		state;
@@ -243,7 +243,7 @@ namespace Avalonia.X11 {
 	internal struct XCreateWindowEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		parent;
 		internal IntPtr		window;
@@ -252,14 +252,14 @@ namespace Avalonia.X11 {
 		internal int		width;
 		internal int		height;
 		internal int		border_width;
-		internal bool		override_redirect;
+		internal int		override_redirect;
 	}
 
 	[StructLayout(LayoutKind.Sequential)]
 	internal struct XDestroyWindowEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		xevent;
 		internal IntPtr		window;
@@ -269,29 +269,29 @@ namespace Avalonia.X11 {
 	internal struct XUnmapEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		xevent;
 		internal IntPtr		window;
-		internal bool		from_configure;
+		internal int		from_configure;
 	}
 
 	[StructLayout(LayoutKind.Sequential)]
 	internal struct XMapEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		xevent;
 		internal IntPtr		window;
-		internal bool		override_redirect;
+		internal int		override_redirect;
 	}
 
 	[StructLayout(LayoutKind.Sequential)]
 	internal struct XMapRequestEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		parent;
 		internal IntPtr		window;
@@ -301,21 +301,21 @@ namespace Avalonia.X11 {
 	internal struct XReparentEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		xevent;
 		internal IntPtr		window;
 		internal IntPtr		parent;
 		internal int		x;
 		internal int		y;
-		internal bool		override_redirect;
+		internal int		override_redirect;
 	}
 
 	[StructLayout(LayoutKind.Sequential)]
 	internal struct XConfigureEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		xevent;
 		internal IntPtr		window;
@@ -325,14 +325,14 @@ namespace Avalonia.X11 {
 		internal int		height;
 		internal int		border_width;
 		internal IntPtr		above;
-		internal bool		override_redirect;
+		internal int		override_redirect;
 	}
 
 	[StructLayout(LayoutKind.Sequential)]
 	internal struct XGravityEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		xevent;
 		internal IntPtr		window;
@@ -344,7 +344,7 @@ namespace Avalonia.X11 {
 	internal struct XResizeRequestEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		window;
 		internal int		width;
@@ -355,7 +355,7 @@ namespace Avalonia.X11 {
 	internal struct XConfigureRequestEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		parent;
 		internal IntPtr		window;
@@ -373,7 +373,7 @@ namespace Avalonia.X11 {
 	internal struct XCirculateEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		xevent;
 		internal IntPtr		window;
@@ -384,7 +384,7 @@ namespace Avalonia.X11 {
 	internal struct XCirculateRequestEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		parent;
 		internal IntPtr		window;
@@ -395,7 +395,7 @@ namespace Avalonia.X11 {
 	internal struct XPropertyEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		window;
 		internal IntPtr		atom;
@@ -407,7 +407,7 @@ namespace Avalonia.X11 {
 	internal struct XSelectionClearEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		window;
 		internal IntPtr		selection;
@@ -418,7 +418,7 @@ namespace Avalonia.X11 {
 	internal struct XSelectionRequestEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		owner;
 		internal IntPtr		requestor;
@@ -432,7 +432,7 @@ namespace Avalonia.X11 {
 	internal struct XSelectionEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		requestor;
 		internal IntPtr		selection;
@@ -445,11 +445,11 @@ namespace Avalonia.X11 {
 	internal struct XColormapEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		window;
 		internal IntPtr		colormap;
-		internal bool		c_new;
+		internal int		c_new;
 		internal int		state;
 	}
 
@@ -457,7 +457,7 @@ namespace Avalonia.X11 {
 	internal struct XClientMessageEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		window;
 		internal IntPtr		message_type;
@@ -473,7 +473,7 @@ namespace Avalonia.X11 {
 	internal struct XMappingEvent {
 		internal XEventName	type;
 		internal IntPtr		serial;
-		internal bool		send_event;
+		internal int		send_event;
 		internal IntPtr		display;
 		internal IntPtr		window;
 		internal int		request;
@@ -518,6 +518,15 @@ namespace Avalonia.X11 {
 		internal IntPtr pad21;
 		internal IntPtr pad22;
 		internal IntPtr pad23;
+		internal IntPtr pad24;
+		internal IntPtr pad25;
+		internal IntPtr pad26;
+		internal IntPtr pad27;
+		internal IntPtr pad28;
+		internal IntPtr pad29;
+		internal IntPtr pad30;
+		internal IntPtr pad31;
+		internal IntPtr pad32;
 	}
 
     [StructLayout(LayoutKind.Sequential)]
@@ -525,7 +534,7 @@ namespace Avalonia.X11 {
     {
         internal int type; /* of event. Always GenericEvent */
         internal IntPtr serial; /* # of last request processed */
-        internal bool send_event; /* true if from SendEvent request */
+        internal int send_event; /* true if from SendEvent request */
         internal IntPtr display; /* Display the event was read from */
         internal int extension; /* major opcode of extension that caused the event */
         internal int evtype; /* actual event type. */
@@ -672,10 +681,10 @@ namespace Avalonia.X11 {
 		internal int		backing_store;
 		internal IntPtr		backing_planes;
 		internal IntPtr		backing_pixel;
-		internal bool		save_under;
+		internal int		save_under;
 		internal IntPtr		event_mask;
 		internal IntPtr		do_not_propagate_mask;
-		internal bool		override_redirect;
+		internal int		override_redirect;
 		internal IntPtr		colormap;
 		internal IntPtr		cursor;
 	}
@@ -696,14 +705,14 @@ namespace Avalonia.X11 {
 		internal int		backing_store;
 		internal IntPtr		backing_planes;
 		internal IntPtr		backing_pixel;
-		internal bool		save_under;
+		internal int		save_under;
 		internal IntPtr		colormap;
-		internal bool		map_installed;
+		internal int		map_installed;
 		internal MapState	map_state;
 		internal IntPtr		all_event_masks;
 		internal IntPtr		your_event_mask;
 		internal IntPtr		do_not_propagate_mask;
-		internal bool		override_direct;
+		internal int		override_direct;
 		internal IntPtr		screen;
 
 		public override string ToString ()
@@ -1029,7 +1038,7 @@ namespace Avalonia.X11 {
 		internal int		max_maps;
 		internal int		min_maps;
 		internal int		backing_store;
-		internal bool		save_unders;
+		internal int		save_unders;
 		internal IntPtr	    root_input_mask;
 	}
 
@@ -1280,7 +1289,7 @@ namespace Avalonia.X11 {
 		internal int			ts_y_origin;
 		internal IntPtr			font;
 		internal GCSubwindowMode	subwindow_mode;
-		internal bool			graphics_exposures;
+		internal int			graphics_exposures;
 		internal int			clip_x_origin;
 		internal int			clib_y_origin;
 		internal IntPtr			clip_mask;
@@ -1499,7 +1508,7 @@ namespace Avalonia.X11 {
 	[StructLayout(LayoutKind.Sequential)]
 	internal struct XWMHints {
 		internal IntPtr			flags;
-		internal bool			input;
+		internal int			input;
 		internal XInitialState		initial_state;
 		internal IntPtr			icon_pixmap;
 		internal IntPtr			icon_window;
@@ -1708,19 +1717,30 @@ namespace Avalonia.X11 {
 	}
 
 	[StructLayout (LayoutKind.Sequential)]
-	internal struct XIMStyles
+	internal unsafe struct XIMStyles
 	{
 		public ushort count_styles;
-		public IntPtr supported_styles;
+		public IntPtr* supported_styles;
 	}
 
 	[StructLayout (LayoutKind.Sequential)]
 	[Serializable]
-	internal class XPoint
+	internal struct XPoint
 	{
 		public short X;
 		public short Y;
 	}
+    
+    [StructLayout (LayoutKind.Sequential)]
+    [Serializable]
+    internal struct XRectangle
+    {
+        public short X;
+        public short Y;
+        public short W;
+        public short H;
+    }
+
 
 	[StructLayout (LayoutKind.Sequential)]
 	[Serializable]
@@ -1798,7 +1818,7 @@ namespace Avalonia.X11 {
 	{
 		public ushort Length;
 		public IntPtr Feedback; // to XIMFeedbackStruct
-		public bool EncodingIsWChar;
+		public int EncodingIsWChar;
 		public IntPtr String; // it could be either char* or wchar_t*
 	}
 
@@ -1850,6 +1870,8 @@ namespace Avalonia.X11 {
 		public const string XNClientWindow = "clientWindow";
 		public const string XNInputStyle = "inputStyle";
 		public const string XNFocusWindow = "focusWindow";
+		public const string XNResourceName = "resourceName";
+		public const string XNResourceClass = "resourceClass";
 
 		// XIMPreeditCallbacks delegate names.
 		public const string XNPreeditStartCallback = "preeditStartCallback";

+ 208 - 0
src/Avalonia.X11/X11Window.Ime.cs

@@ -0,0 +1,208 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.FreeDesktop;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+using Avalonia.Platform.Interop;
+using static Avalonia.X11.XLib;
+
+namespace Avalonia.X11
+{
+    partial class X11Window
+    {
+        private ITextInputMethodImpl _ime;
+        private IX11InputMethodControl _imeControl;
+        private bool _processingIme;
+
+        private Queue<(RawKeyEventArgs args, XEvent xev, int keyval, int keycode)> _imeQueue =
+            new Queue<(RawKeyEventArgs args, XEvent xev, int keyVal, int keyCode)>();
+
+        unsafe void CreateIC()
+        {
+            if (_x11.HasXim)
+            {
+                XGetIMValues(_x11.Xim, XNames.XNQueryInputStyle, out var supported_styles, IntPtr.Zero);
+                for (var c = 0; c < supported_styles->count_styles; c++)
+                {
+                    var style = (XIMProperties)supported_styles->supported_styles[c];
+                    if ((int)(style & XIMProperties.XIMPreeditPosition) != 0
+                        && ((int)(style & XIMProperties.XIMStatusNothing) != 0))
+                    {
+                        XPoint spot = default;
+                        XRectangle area = default;
+
+
+                        //using var areaS = new Utf8Buffer("area");
+                        using var spotS = new Utf8Buffer("spotLocation");
+                        using var fontS = new Utf8Buffer("fontSet");
+
+                        var list = XVaCreateNestedList(0,
+                            //areaS, &area,
+                            spotS, &spot,
+                            fontS, _x11.DefaultFontSet,
+                            IntPtr.Zero);
+                        _xic = XCreateIC(_x11.Xim,
+                            XNames.XNClientWindow, _handle,
+                            XNames.XNFocusWindow, _handle,
+                            XNames.XNInputStyle, new IntPtr((int)style),
+                            XNames.XNResourceName, _platform.Options.WmClass,
+                            XNames.XNResourceClass, _platform.Options.WmClass,
+                            XNames.XNPreeditAttributes, list,
+                            IntPtr.Zero);
+
+                        XFree(list);
+
+                        break;
+                    }
+                }
+                
+                XFree(new IntPtr(supported_styles));
+            }
+            
+            if (_xic == IntPtr.Zero)
+                _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle,
+                    new IntPtr((int)(XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing)),
+                    XNames.XNClientWindow, _handle, XNames.XNFocusWindow, _handle, IntPtr.Zero);
+        }
+        
+        void InitializeIme()
+        {
+            var ime =  AvaloniaLocator.Current.GetService<IX11InputMethodFactory>()?.CreateClient(_handle);
+            if (ime == null && _x11.HasXim)
+            {
+                var xim = new XimInputMethod(this);
+                ime = (xim, xim);
+            }
+            if (ime != null)
+            {
+                (_ime, _imeControl) = ime.Value;
+                _imeControl.Commit += s =>
+                    ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)_x11.LastActivityTimestamp.ToInt64(),
+                        _inputRoot, s));
+                _imeControl.ForwardKey += ev =>
+                {
+                    ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)_x11.LastActivityTimestamp.ToInt64(),
+                        _inputRoot, ev.Type, X11KeyTransform.ConvertKey((X11Key)ev.KeyVal),
+                        (RawInputModifiers)ev.Modifiers));
+                };
+            }
+        }
+
+        void UpdateImePosition() => _imeControl?.UpdateWindowInfo(Position, RenderScaling);
+
+        void HandleKeyEvent(ref XEvent ev)
+        {
+            var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask);
+
+            // We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway
+            var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32();
+                
+            // Manually switch the Shift index for the keypad,
+            // there should be a proper way to do this
+            if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask)
+                && key > X11Key.Num_Lock && key <= X11Key.KP_9)
+                key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32();
+            
+            var filtered = ScheduleKeyInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot,
+                ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
+                X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev, (int)key, ev.KeyEvent.keycode);
+           
+            if (ev.type == XEventName.KeyPress && !filtered) 
+                TriggerClassicTextInputEvent(ref ev);
+        }
+
+        void TriggerClassicTextInputEvent(ref XEvent ev)
+        {
+            var text = TranslateEventToString(ref ev);
+            if (text != null)
+                ScheduleInput(
+                    new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, text),
+                    ref ev);
+        }
+
+        private const int ImeBufferSize = 64 * 1024;
+        [ThreadStatic] private static IntPtr ImeBuffer;
+        
+        unsafe string TranslateEventToString(ref XEvent ev)
+        {
+            if (ImeBuffer == IntPtr.Zero)
+                ImeBuffer = Marshal.AllocHGlobal(ImeBufferSize);
+            
+            var len = Xutf8LookupString(_xic, ref ev, ImeBuffer.ToPointer(), ImeBufferSize, 
+                out _, out var istatus);
+            var status = (XLookupStatus)istatus;
+
+            if (len == 0)
+                return null;
+
+            string text;
+            if (status == XLookupStatus.XBufferOverflow)
+                return null;
+            else
+                text = Encoding.UTF8.GetString((byte*)ImeBuffer.ToPointer(), len);
+
+            if (text == null)
+                return null;
+            
+            if (text.Length == 1)
+            {
+                if (text[0] < ' ' || text[0] == 0x7f) //Control codes or DEL
+                    return null;
+            }
+
+            return text;
+        }
+        
+        
+        bool ScheduleKeyInput(RawKeyEventArgs args, ref XEvent xev, int keyval, int keycode)
+        {
+            _x11.LastActivityTimestamp = xev.ButtonEvent.time;
+            if (_imeControl != null && _imeControl.IsEnabled)
+            {
+                if (FilterIme(args, xev, keyval, keycode))
+                    return true;
+            }
+            ScheduleInput(args);
+            return false;
+        }
+        
+        bool FilterIme(RawKeyEventArgs args, XEvent xev, int keyval, int keycode)
+        {
+            if (_ime == null)
+                return false;
+            _imeQueue.Enqueue((args, xev, keyval, keycode));
+            if (!_processingIme)
+                ProcessNextImeEvent();
+
+            return true;
+        }
+
+        async void ProcessNextImeEvent()
+        {
+            if(_processingIme)
+                return;
+            _processingIme = true;
+            try
+            {
+                while (_imeQueue.Count != 0)
+                {
+                    var ev = _imeQueue.Dequeue();
+                    if (_imeControl == null || !await _imeControl.HandleEventAsync(ev.args, ev.keyval, ev.keycode))
+                    {
+                        ScheduleInput(ev.args);
+                        if (ev.args.Type == RawKeyEventType.KeyDown)
+                            TriggerClassicTextInputEvent(ref ev.xev);
+                    }
+                }
+            }
+            finally
+            {
+                _processingIme = false;
+            }
+        }
+    }
+}

+ 121 - 0
src/Avalonia.X11/X11Window.Xim.cs

@@ -0,0 +1,121 @@
+using System;
+using System.Threading.Tasks;
+using Avalonia.FreeDesktop;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+using Avalonia.Platform.Interop;
+using Avalonia.Threading;
+using static Avalonia.X11.XLib;
+namespace Avalonia.X11
+{
+    partial class X11Window
+    {
+
+        class XimInputMethod : ITextInputMethodImpl, IX11InputMethodControl
+        {
+            private readonly X11Window _parent;
+            private bool _controlActive, _windowActive, _imeActive;
+            private Rect? _queuedCursorRect;
+
+            public XimInputMethod(X11Window parent)
+            {
+                _parent = parent;
+            }
+            
+            public void SetCursorRect(Rect rect)
+            {
+                var needEnqueue = _queuedCursorRect == null;
+                _queuedCursorRect = rect;
+                if(needEnqueue)
+                    Dispatcher.UIThread.Post(() =>
+                    {
+                        if(_queuedCursorRect == null)
+                            return;
+                        var rc = _queuedCursorRect.Value;
+                        _queuedCursorRect = null;
+
+                        if (_parent._xic == IntPtr.Zero)
+                            return;
+
+                        rect *= _parent._scaling;
+                        
+                        var pt = new XPoint
+                        {
+                            X = (short)Math.Min(Math.Max(rect.X, short.MinValue), short.MaxValue),
+                            Y = (short)Math.Min(Math.Max(rect.Y + rect.Height, short.MinValue), short.MaxValue)
+                        };
+
+                        using var spotLoc = new Utf8Buffer(XNames.XNSpotLocation);
+                        var list = XVaCreateNestedList(0, spotLoc, ref pt, IntPtr.Zero);
+                        XSetICValues(_parent._xic, XNames.XNPreeditAttributes, list, IntPtr.Zero);
+                        XFree(list);
+                    }, DispatcherPriority.Background);
+            }
+            
+            public void SetWindowActive(bool active)
+            {
+                _windowActive = active;
+                UpdateActive();
+            }
+
+            public void SetActive(bool active)
+            {
+                _controlActive = active;
+                UpdateActive();
+            }
+
+            private void UpdateActive()
+            {
+                var active = _windowActive && _controlActive;
+                if(_parent._xic == IntPtr.Zero)
+                    return;
+                if (active != _imeActive)
+                {
+                    _imeActive = active;
+                    if (active)
+                    {
+                        Reset();
+                        XSetICFocus(_parent._xic);
+                    }
+                    else
+                        XUnsetICFocus(_parent._xic);
+                }
+            }
+            
+            public void UpdateWindowInfo(PixelPoint position, double scaling)
+            {
+                // No-op
+            }
+            
+            public void SetOptions(TextInputOptionsQueryEventArgs options)
+            {
+                // No-op
+            }
+
+            public void Reset()
+            {
+                if(_parent._xic == IntPtr.Zero)
+                    return;
+                
+                var data = XmbResetIC(_parent._xic);
+                if (data != IntPtr.Zero)
+                    XFree(data);
+            }
+
+            public void Dispose()
+            {
+                // No-op
+            }
+
+            public bool IsEnabled => false;
+
+            public ValueTask<bool> HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode) =>
+                new ValueTask<bool>(false);
+
+            public event Action<string> Commit;
+            public event Action<X11InputMethodForwardedKey> ForwardKey;
+
+        }
+        
+    }
+}

+ 34 - 46
src/Avalonia.X11/X11Window.cs

@@ -5,12 +5,14 @@ using System.Diagnostics;
 using System.Linq;
 using System.Reactive.Disposables;
 using System.Text;
+using System.Threading.Tasks;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
 using Avalonia.Controls.Primitives.PopupPositioning;
 using Avalonia.FreeDesktop;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
 using Avalonia.OpenGL;
 using Avalonia.OpenGL.Egl;
 using Avalonia.Platform;
@@ -22,9 +24,10 @@ using static Avalonia.X11.XLib;
 // ReSharper disable StringLiteralTypo
 namespace Avalonia.X11
 {
-    unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client,
+    unsafe partial class X11Window : IWindowImpl, IPopupImpl, IXI2Client,
         ITopLevelImplWithNativeMenuExporter,
-        ITopLevelImplWithNativeControlHost
+        ITopLevelImplWithNativeControlHost,
+        ITopLevelImplWithTextInputMethod
     {
         private readonly AvaloniaX11Platform _platform;
         private readonly IWindowImpl _popupParent;
@@ -79,7 +82,7 @@ namespace Avalonia.X11
 
             if (_popup)
             {
-                attr.override_redirect = true;
+                attr.override_redirect = 1;
                 valueMask |= SetWindowValuemask.OverrideRedirect;
             }
 
@@ -178,11 +181,12 @@ namespace Avalonia.X11
             Surfaces = surfaces.ToArray();
             UpdateMotifHints();
             UpdateSizeHints(null);
-            _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing,
-                XNames.XNClientWindow, _handle, IntPtr.Zero);
+
             _transparencyHelper = new TransparencyHelper(_x11, _handle, platform.Globals);
             _transparencyHelper.SetTransparencyRequest(WindowTransparencyLevel.None);
 
+            CreateIC();
+
             XFlush(_x11.Display);
             if(_popup)
                 PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize));
@@ -194,6 +198,7 @@ namespace Avalonia.X11
                 Paint?.Invoke(default);
                 return _handle != IntPtr.Zero;
             }, TimeSpan.FromMilliseconds(100));
+            InitializeIme();
         }
 
         class SurfaceInfo  : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo
@@ -350,15 +355,13 @@ namespace Avalonia.X11
                 (IRenderer)new X11ImmediateRendererProxy(root, loop);
         }
 
-        void OnEvent(XEvent ev)
+        void OnEvent(ref XEvent ev)
         {
             lock (SyncRoot)
-                OnEventSync(ev);
+                OnEventSync(ref ev);
         }
-        void OnEventSync(XEvent ev)
+        void OnEventSync(ref XEvent ev)
         {
-            if(XFilterEvent(ref ev, _handle))
-                return;
             if (ev.type == XEventName.MapNotify)
             {
                 _mapped = true;
@@ -386,9 +389,13 @@ namespace Avalonia.X11
                 if (ActivateTransientChildIfNeeded())
                     return;
                 Activated?.Invoke();
+                _imeControl?.SetWindowActive(true);
             }
             else if (ev.type == XEventName.FocusOut)
+            {
+                _imeControl?.SetWindowActive(false);
                 Deactivated?.Invoke();
+            }
             else if (ev.type == XEventName.MotionNotify)
                 MouseEvent(RawPointerEventType.Move, ref ev, ev.MotionEvent.state);
             else if (ev.type == XEventName.LeaveNotify)
@@ -447,7 +454,7 @@ namespace Avalonia.X11
                     return;
                 var needEnqueue = (_configure == null);
                 _configure = ev.ConfigureEvent;
-                if (ev.ConfigureEvent.override_redirect || ev.ConfigureEvent.send_event)
+                if (ev.ConfigureEvent.override_redirect != 0  || ev.ConfigureEvent.send_event != 0)
                     _configurePoint = new PixelPoint(ev.ConfigureEvent.x, ev.ConfigureEvent.y);
                 else
                 {
@@ -477,6 +484,7 @@ namespace Avalonia.X11
                             PositionChanged?.Invoke(npos);
                             updatedSizeViaScaling = UpdateScaling();
                         }
+                        UpdateImePosition();
 
                         if (changedSize && !updatedSizeViaScaling && !_popup)
                             Resized?.Invoke(ClientSize);
@@ -487,7 +495,8 @@ namespace Avalonia.X11
                     XConfigureResizeWindow(_x11.Display, _renderHandle, ev.ConfigureEvent.width,
                         ev.ConfigureEvent.height);
             }
-            else if (ev.type == XEventName.DestroyNotify && ev.AnyEvent.window == _handle)
+            else if (ev.type == XEventName.DestroyNotify 
+                     && ev.DestroyWindowEvent.window == _handle)
             {
                 Cleanup();
             }
@@ -507,39 +516,7 @@ namespace Avalonia.X11
             {
                 if (ActivateTransientChildIfNeeded())
                     return;
-                var buffer = stackalloc byte[40];
-
-                var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask);
-                
-                // We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway
-                var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32();
-                
-                // Manually switch the Shift index for the keypad,
-                // there should be a proper way to do this
-                if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask)
-                    && key > X11Key.Num_Lock && key <= X11Key.KP_9)
-                    key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32();
-                
-                
-                ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot,
-                    ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
-                    X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev);
-
-                if (ev.type == XEventName.KeyPress)
-                {
-                    var len = Xutf8LookupString(_xic, ref ev, buffer, 40, out _, out _);
-                    if (len != 0)
-                    {
-                        var text = Encoding.UTF8.GetString(buffer, len);
-                        if (text.Length == 1)
-                        {
-                            if (text[0] < ' ' || text[0] == 0x7f) //Control codes or DEL
-                                return;
-                        }
-                        ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, text),
-                            ref ev);
-                    }
-                }
+                HandleKeyEvent(ref ev);
             }
         }
 
@@ -562,6 +539,7 @@ namespace Avalonia.X11
                     var oldScaledSize = ClientSize;
                     RenderScaling = newScaling;
                     ScalingChanged?.Invoke(RenderScaling);
+                    UpdateImePosition();
                     SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize);
                     if(!skipResize)
                         Resize(oldScaledSize, true);
@@ -699,6 +677,7 @@ namespace Avalonia.X11
             _x11.LastActivityTimestamp = xev.ButtonEvent.time;
             ScheduleInput(args);
         }
+        
 
         public void ScheduleXI2Input(RawInputEventArgs args)
         {
@@ -781,6 +760,13 @@ namespace Avalonia.X11
 
         void Cleanup()
         {
+            if (_imeControl != null)
+            {
+                _imeControl.Dispose();
+                _imeControl = null;
+                _ime = null;
+            }
+            
             if (_xic != IntPtr.Zero)
             {
                 XDestroyIC(_xic);
@@ -957,7 +943,7 @@ namespace Avalonia.X11
                 ClientMessageEvent =
                 {
                     type = XEventName.ClientMessage,
-                    send_event = true,
+                    send_event = 1,
                     window = _handle,
                     message_type = message_type,
                     format = 32,
@@ -1130,6 +1116,8 @@ namespace Avalonia.X11
         public IPopupPositioner PopupPositioner { get; }
         public ITopLevelNativeMenuExporter NativeMenuExporter { get; }
         public INativeControlHostImpl NativeControlHost { get; }
+        public ITextInputMethodImpl TextInputMethod => _ime;
+
         public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) =>
             _transparencyHelper.SetTransparencyRequest(transparencyLevel);
 

+ 57 - 9
src/Avalonia.X11/XLib.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Runtime.InteropServices;
 using System.Text;
+using Avalonia.Platform.Interop;
 
 // ReSharper disable MemberCanBePrivate.Global
 // ReSharper disable FieldCanBeMadeReadOnly.Global
@@ -57,6 +58,9 @@ namespace Avalonia.X11
 
         [DllImport(libX11)]
         public static extern IntPtr XNextEvent(IntPtr display, out XEvent xevent);
+        
+        [DllImport(libX11)]
+        public static extern IntPtr XNextEvent(IntPtr display, XEvent* xevent);
 
         [DllImport(libX11)]
         public static extern int XConnectionNumber(IntPtr diplay);
@@ -407,6 +411,9 @@ namespace Avalonia.X11
 
         [DllImport(libX11)]
         public static extern bool XFilterEvent(ref XEvent xevent, IntPtr window);
+        
+        [DllImport(libX11)]
+        public static extern bool XFilterEvent(XEvent* xevent, IntPtr window);
 
         [DllImport(libX11)]
         public static extern void XkbSetDetectableAutoRepeat(IntPtr display, bool detectable, IntPtr supported);
@@ -441,9 +448,9 @@ namespace Avalonia.X11
         [DllImport(libX11)]
         public static extern IntPtr XCreateColormap(IntPtr display, IntPtr window, IntPtr visual, int create);
         
-        public enum XLookupStatus
+        public enum XLookupStatus : uint
         {
-            XBufferOverflow = -1,
+            XBufferOverflow = 0xffffffffu,
             XLookupNone = 1,
             XLookupChars = 2,
             XLookupKeySym = 3,
@@ -454,7 +461,10 @@ namespace Avalonia.X11
         public static extern unsafe int XLookupString(ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status);
         
         [DllImport (libX11)]
-        public static extern unsafe int Xutf8LookupString(IntPtr xic, ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status);
+        public static extern unsafe int Xutf8LookupString(IntPtr xic, ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out UIntPtr status);
+        
+        [DllImport (libX11)]
+        public static extern unsafe int Xutf8LookupString(IntPtr xic, XEvent* xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status);
         
         [DllImport (libX11)]
         public static extern unsafe IntPtr XKeycodeToKeysym(IntPtr display, int keycode, int index);
@@ -464,12 +474,52 @@ namespace Avalonia.X11
 
         [DllImport (libX11)]
         public static extern IntPtr XOpenIM (IntPtr display, IntPtr rdb, IntPtr res_name, IntPtr res_class);
-
+        
         [DllImport (libX11)]
-        public static extern IntPtr XCreateIC (IntPtr xim, string name, XIMProperties im_style, string name2, IntPtr value2, IntPtr terminator);
+        public static extern IntPtr XGetIMValues (IntPtr xim, string name, out XIMStyles* value, IntPtr terminator);
+        
+        [DllImport (libX11)]
+        public static extern IntPtr XCreateIC (IntPtr xim, string name, IntPtr value, string name2, IntPtr value2, string name3, IntPtr value3, IntPtr terminator);
+
+        [DllImport(libX11)]
+        public static extern IntPtr XCreateIC(IntPtr xim, string name, IntPtr value, string name2, IntPtr value2,
+            string name3, IntPtr value3, string name4, IntPtr value4, IntPtr terminator);
+
+        [DllImport(libX11)]
+        public static extern IntPtr XCreateIC(IntPtr xim, string xnClientWindow, IntPtr handle, 
+            string xnInputStyle, IntPtr value3, string xnResourceName, string optionsWmClass,
+            string xnResourceClass, string wmClass, string xnPreeditAttributes, IntPtr list, IntPtr zero);
+        
+        [DllImport(libX11)]
+        public static extern IntPtr XCreateIC(IntPtr xim, string xnClientWindow, IntPtr handle, string xnFocusWindow,
+            IntPtr value2, string xnInputStyle, IntPtr value3, string xnResourceName, string optionsWmClass,
+            string xnResourceClass, string wmClass, string xnPreeditAttributes, IntPtr list, IntPtr zero);
+
+        [DllImport(libX11)]
+        public static extern void XSetICFocus(IntPtr xic);
+        
+        [DllImport(libX11)]
+        public static extern void XUnsetICFocus(IntPtr xic);
+        
+        [DllImport(libX11)]
+        public static extern IntPtr XmbResetIC(IntPtr xic);
+
+        [DllImport(libX11)]
+        public static extern IntPtr XVaCreateNestedList(int unused, Utf8Buffer name, ref XPoint point, IntPtr terminator);
+        
+        [DllImport(libX11)]
+        public static extern IntPtr XVaCreateNestedList(int unused, Utf8Buffer xnArea, XRectangle* point,
+            Utf8Buffer xnSpotLocation, XPoint* value2, Utf8Buffer xnFontSet, IntPtr fs, IntPtr zero);
+        
+        [DllImport(libX11)]
+        public static extern IntPtr XVaCreateNestedList(int unused,
+            Utf8Buffer xnSpotLocation, XPoint* value2, Utf8Buffer xnFontSet, IntPtr fs, IntPtr zero);
         
         [DllImport (libX11)]
-        public static extern IntPtr XCreateIC (IntPtr xim, string name, XIMProperties im_style, string name2, IntPtr value2, string name3, IntPtr value3, IntPtr terminator);
+        public static extern IntPtr XCreateFontSet (IntPtr display, string name, out IntPtr list, out int count, IntPtr unused);
+        
+        [DllImport(libX11)]
+        public static extern IntPtr XSetICValues(IntPtr ic, string name, IntPtr data, IntPtr terminator);
         
         [DllImport (libX11)]
         public static extern void XCloseIM (IntPtr xim);
@@ -626,14 +676,12 @@ namespace Avalonia.X11
             }
         }
 
-        public static IntPtr CreateEventWindow(AvaloniaX11Platform plat, Action<XEvent> handler)
+        public static IntPtr CreateEventWindow(AvaloniaX11Platform plat, X11PlatformThreading.EventHandler handler)
         {
             var win = XCreateSimpleWindow(plat.Display, plat.Info.DefaultRootWindow, 
                 0, 0, 1, 1, 0, IntPtr.Zero, IntPtr.Zero);
             plat.Windows[win] = handler;
             return win;
         }
-
-
     }
 }