// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.InteropServices; using System.Text; namespace Microsoft.Maui.Platform.Linux.Services; /// /// X11 Input Method service using XIM protocol. /// Provides IME support for CJK and other complex input methods. /// public class X11InputMethodService : IInputMethodService, IDisposable { private nint _display; private nint _window; private nint _xim; private nint _xic; private IInputContext? _currentContext; private string _preEditText = string.Empty; private int _preEditCursorPosition; private bool _isActive; private bool _disposed; // XIM callback delegates (prevent GC) private XIMProc? _preeditStartCallback; private XIMProc? _preeditDoneCallback; private XIMProc? _preeditDrawCallback; private XIMProc? _preeditCaretCallback; private XIMProc? _commitCallback; public bool IsActive => _isActive; public string PreEditText => _preEditText; public int PreEditCursorPosition => _preEditCursorPosition; public event EventHandler? TextCommitted; public event EventHandler? PreEditChanged; public event EventHandler? PreEditEnded; public void Initialize(nint windowHandle) { _window = windowHandle; // Get display from X11 interop _display = XOpenDisplay(IntPtr.Zero); if (_display == IntPtr.Zero) { Console.WriteLine("X11InputMethodService: Failed to open display"); return; } // Set locale for proper IME operation if (XSetLocaleModifiers("") == IntPtr.Zero) { XSetLocaleModifiers("@im=none"); } // Open input method _xim = XOpenIM(_display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); if (_xim == IntPtr.Zero) { Console.WriteLine("X11InputMethodService: No input method available, trying IBus..."); TryIBusFallback(); return; } CreateInputContext(); } private void CreateInputContext() { if (_xim == IntPtr.Zero || _window == IntPtr.Zero) return; // Create input context with preedit callbacks var preeditAttr = CreatePreeditAttributes(); _xic = XCreateIC(_xim, XNClientWindow, _window, XNFocusWindow, _window, XNInputStyle, XIMPreeditCallbacks | XIMStatusNothing, XNPreeditAttributes, preeditAttr, IntPtr.Zero); if (preeditAttr != IntPtr.Zero) { XFree(preeditAttr); } if (_xic == IntPtr.Zero) { // Fallback to simpler input style _xic = XCreateICSimple(_xim, XNClientWindow, _window, XNFocusWindow, _window, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, IntPtr.Zero); } if (_xic != IntPtr.Zero) { Console.WriteLine("X11InputMethodService: Input context created successfully"); } } private nint CreatePreeditAttributes() { // Set up preedit callbacks for on-the-spot composition _preeditStartCallback = PreeditStartCallback; _preeditDoneCallback = PreeditDoneCallback; _preeditDrawCallback = PreeditDrawCallback; _preeditCaretCallback = PreeditCaretCallback; // Create callback structures // Note: Actual implementation would marshal XIMCallback structures return IntPtr.Zero; } private int PreeditStartCallback(nint xic, nint clientData, nint callData) { _isActive = true; _preEditText = string.Empty; _preEditCursorPosition = 0; return -1; // No length limit } private int PreeditDoneCallback(nint xic, nint clientData, nint callData) { _isActive = false; _preEditText = string.Empty; _preEditCursorPosition = 0; PreEditEnded?.Invoke(this, EventArgs.Empty); _currentContext?.OnPreEditEnded(); return 0; } private int PreeditDrawCallback(nint xic, nint clientData, nint callData) { // Parse XIMPreeditDrawCallbackStruct // Update preedit text and cursor position // This would involve marshaling the callback data structure PreEditChanged?.Invoke(this, new PreEditChangedEventArgs(_preEditText, _preEditCursorPosition)); _currentContext?.OnPreEditChanged(_preEditText, _preEditCursorPosition); return 0; } private int PreeditCaretCallback(nint xic, nint clientData, nint callData) { // Handle caret movement in preedit text return 0; } private void TryIBusFallback() { // Try to connect to IBus via D-Bus // This provides a more modern IME interface Console.WriteLine("X11InputMethodService: IBus fallback not yet implemented"); } public void SetFocus(IInputContext? context) { _currentContext = context; if (_xic != IntPtr.Zero) { if (context != null) { XSetICFocus(_xic); } else { XUnsetICFocus(_xic); } } } public void SetCursorLocation(int x, int y, int width, int height) { if (_xic == IntPtr.Zero) return; // Set the spot location for candidate window positioning var spotLocation = new XPoint { x = (short)x, y = (short)y }; var attr = XVaCreateNestedList(0, XNSpotLocation, ref spotLocation, IntPtr.Zero); if (attr != IntPtr.Zero) { XSetICValues(_xic, XNPreeditAttributes, attr, IntPtr.Zero); XFree(attr); } } public bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown) { if (_xic == IntPtr.Zero) return false; // Convert to X11 key event var xEvent = new XKeyEvent { type = isKeyDown ? KeyPress : KeyRelease, display = _display, window = _window, state = ConvertModifiers(modifiers), keycode = keyCode }; // Filter through XIM if (XFilterEvent(ref xEvent, _window)) { return true; // Event consumed by IME } // If not filtered and key down, try to get committed text if (isKeyDown) { var buffer = new byte[64]; var keySym = IntPtr.Zero; var status = IntPtr.Zero; int len = Xutf8LookupString(_xic, ref xEvent, buffer, buffer.Length, ref keySym, ref status); if (len > 0) { string text = Encoding.UTF8.GetString(buffer, 0, len); OnTextCommit(text); return true; } } return false; } private void OnTextCommit(string text) { _preEditText = string.Empty; _preEditCursorPosition = 0; TextCommitted?.Invoke(this, new TextCommittedEventArgs(text)); _currentContext?.OnTextCommitted(text); } private uint ConvertModifiers(KeyModifiers modifiers) { uint state = 0; if (modifiers.HasFlag(KeyModifiers.Shift)) state |= ShiftMask; if (modifiers.HasFlag(KeyModifiers.Control)) state |= ControlMask; if (modifiers.HasFlag(KeyModifiers.Alt)) state |= Mod1Mask; if (modifiers.HasFlag(KeyModifiers.Super)) state |= Mod4Mask; if (modifiers.HasFlag(KeyModifiers.CapsLock)) state |= LockMask; if (modifiers.HasFlag(KeyModifiers.NumLock)) state |= Mod2Mask; return state; } public void Reset() { if (_xic != IntPtr.Zero) { XmbResetIC(_xic); } _preEditText = string.Empty; _preEditCursorPosition = 0; _isActive = false; PreEditEnded?.Invoke(this, EventArgs.Empty); _currentContext?.OnPreEditEnded(); } public void Shutdown() { Dispose(); } public void Dispose() { if (_disposed) return; _disposed = true; if (_xic != IntPtr.Zero) { XDestroyIC(_xic); _xic = IntPtr.Zero; } if (_xim != IntPtr.Zero) { XCloseIM(_xim); _xim = IntPtr.Zero; } // Note: Don't close display here if shared with window } #region X11 Interop private const int KeyPress = 2; private const int KeyRelease = 3; private const uint ShiftMask = 1 << 0; private const uint LockMask = 1 << 1; private const uint ControlMask = 1 << 2; private const uint Mod1Mask = 1 << 3; // Alt private const uint Mod2Mask = 1 << 4; // NumLock private const uint Mod4Mask = 1 << 6; // Super private const long XIMPreeditNothing = 0x0008L; private const long XIMPreeditCallbacks = 0x0002L; private const long XIMStatusNothing = 0x0400L; private static readonly nint XNClientWindow = Marshal.StringToHGlobalAnsi("clientWindow"); private static readonly nint XNFocusWindow = Marshal.StringToHGlobalAnsi("focusWindow"); private static readonly nint XNInputStyle = Marshal.StringToHGlobalAnsi("inputStyle"); private static readonly nint XNPreeditAttributes = Marshal.StringToHGlobalAnsi("preeditAttributes"); private static readonly nint XNSpotLocation = Marshal.StringToHGlobalAnsi("spotLocation"); private delegate int XIMProc(nint xic, nint clientData, nint callData); [StructLayout(LayoutKind.Sequential)] private struct XPoint { public short x; public short y; } [StructLayout(LayoutKind.Sequential)] private struct XKeyEvent { public int type; public ulong serial; public bool send_event; public nint display; public nint window; public nint root; public nint subwindow; public ulong time; public int x, y; public int x_root, y_root; public uint state; public uint keycode; public bool same_screen; } [DllImport("libX11.so.6")] private static extern nint XOpenDisplay(nint display); [DllImport("libX11.so.6")] private static extern nint XSetLocaleModifiers(string modifiers); [DllImport("libX11.so.6")] private static extern nint XOpenIM(nint display, nint db, nint res_name, nint res_class); [DllImport("libX11.so.6")] private static extern void XCloseIM(nint xim); [DllImport("libX11.so.6", EntryPoint = "XCreateIC")] private static extern nint XCreateIC(nint xim, nint name1, nint value1, nint name2, nint value2, nint name3, long value3, nint name4, nint value4, nint terminator); [DllImport("libX11.so.6", EntryPoint = "XCreateIC")] private static extern nint XCreateICSimple(nint xim, nint name1, nint value1, nint name2, nint value2, nint name3, long value3, nint terminator); [DllImport("libX11.so.6")] private static extern void XDestroyIC(nint xic); [DllImport("libX11.so.6")] private static extern void XSetICFocus(nint xic); [DllImport("libX11.so.6")] private static extern void XUnsetICFocus(nint xic); [DllImport("libX11.so.6")] private static extern nint XSetICValues(nint xic, nint name, nint value, nint terminator); [DllImport("libX11.so.6")] private static extern nint XVaCreateNestedList(int unused, nint name, ref XPoint value, nint terminator); [DllImport("libX11.so.6")] private static extern bool XFilterEvent(ref XKeyEvent xevent, nint window); [DllImport("libX11.so.6")] private static extern int Xutf8LookupString(nint xic, ref XKeyEvent xevent, byte[] buffer, int bytes, ref nint keySym, ref nint status); [DllImport("libX11.so.6")] private static extern nint XmbResetIC(nint xic); [DllImport("libX11.so.6")] private static extern void XFree(nint ptr); #endregion }