Browse Source

add implementation of advanced ios ime api.

Dan Walmsley 3 years ago
parent
commit
71ed1b03c1
2 changed files with 372 additions and 53 deletions
  1. 371 53
      src/iOS/Avalonia.iOS/AvaloniaView.Text.cs
  2. 1 0
      src/iOS/Avalonia.iOS/AvaloniaView.cs

+ 371 - 53
src/iOS/Avalonia.iOS/AvaloniaView.Text.cs

@@ -1,75 +1,66 @@
+using System;
 using Foundation;
 using ObjCRuntime;
 using Avalonia.Input.TextInput;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
+using CoreGraphics;
 using UIKit;
 
 namespace Avalonia.iOS;
 
 #nullable enable
 
+[Adopts("UITextInput")]
 [Adopts("UITextInputTraits")]
 [Adopts("UIKeyInput")]
-public partial class AvaloniaView : ITextInputMethodImpl
+public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput
 {
-    private IUITextInputDelegate _inputDelegate;
-    private ITextInputMethodClient? _client;
-
-    class TextInputHandler : UITextInputDelegate
+    private class AvaloniaTextRange : UITextRange
     {
-    }
-    
-    public ITextInputMethodClient? Client => _client;
-    public bool IsActive => _client != null;
-    public override bool CanResignFirstResponder => true;
-    public override bool CanBecomeFirstResponder => true;
+        private readonly AvaloniaTextPosition _start;
+        private readonly AvaloniaTextPosition _end;
 
-    [Export("hasText")]
-    public bool HasText
-    {
-        get
+        public AvaloniaTextRange(int start, int end)
         {
-            if (Client is { } && Client.SupportsSurroundingText &&
-                Client.SurroundingText.Text.Length > 0)
-            {
-                return true;
-            }
-
-            return false;
+            _start = new AvaloniaTextPosition(start);
+            _end = new AvaloniaTextPosition(end);
         }
-    }
 
-    [Export("keyboardType")] public UIKeyboardType KeyboardType { get; private set; } = UIKeyboardType.Default;
+        public override AvaloniaTextPosition Start => _start;
 
-    [Export("isSecureTextEntry")] public bool IsSecureEntry { get; private set; }
+        public override AvaloniaTextPosition End => _end;
+    }
 
-    [Export("insertText:")]
-    public void InsertText(string text)
+    private class AvaloniaTextPosition : UITextPosition
     {
-        if (KeyboardDevice.Instance is { })
+        public AvaloniaTextPosition(int offset)
         {
-            _topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance,
-                0, InputRoot, text));
+            Offset = offset;
         }
+
+        public int Offset { get; }
     }
 
-    public IUITextInputDelegate InputDelegate => _inputDelegate;
+    private string _markedText = "";
+    private readonly IUITextInputDelegate _inputDelegate;
+    private ITextInputMethodClient? _client;
+    private NSDictionary? _markedTextStyle;
+    private readonly UITextPosition _beginningOfDocument = new AvaloniaTextPosition(0);
+    private readonly UITextInputStringTokenizer _tokenizer;
 
-    [Export("deleteBackward")]
-    public void DeleteBackward()
+    private class TextInputHandler : UITextInputDelegate
     {
-        if (KeyboardDevice.Instance is { })
-        {
-            // TODO: pass this through IME infrastructure instead of emulating a backspace press
-            _topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance,
-                0, InputRoot, RawKeyEventType.KeyDown, Key.Back, RawInputModifiers.None));
-
-            _topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance,
-                0, InputRoot, RawKeyEventType.KeyUp, Key.Back, RawInputModifiers.None));
-        }
     }
 
+    public ITextInputMethodClient? Client => _client;
+
+    public bool IsActive => _client != null;
+
+    public override bool CanResignFirstResponder => true;
+
+    public override bool CanBecomeFirstResponder => true;
+
     void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client)
     {
         _client = client;
@@ -86,36 +77,36 @@ public partial class AvaloniaView : ITextInputMethodImpl
 
     void ITextInputMethodImpl.SetCursorRect(Rect rect)
     {
-
+        // maybe this will be cursor / selection rect?
     }
 
     void ITextInputMethodImpl.SetOptions(TextInputOptions options)
     {
         IsSecureEntry = false;
-        
+
         switch (options.ContentType)
         {
             case TextInputContentType.Normal:
                 KeyboardType = UIKeyboardType.Default;
                 break;
-            
+
             case TextInputContentType.Alpha:
                 KeyboardType = UIKeyboardType.AsciiCapable;
                 break;
-            
+
             case TextInputContentType.Digits:
                 KeyboardType = UIKeyboardType.PhonePad;
                 break;
-            
+
             case TextInputContentType.Pin:
                 KeyboardType = UIKeyboardType.NumberPad;
                 IsSecureEntry = true;
                 break;
-            
+
             case TextInputContentType.Number:
                 KeyboardType = UIKeyboardType.PhonePad;
                 break;
-            
+
             case TextInputContentType.Email:
                 KeyboardType = UIKeyboardType.EmailAddress;
                 break;
@@ -123,20 +114,20 @@ public partial class AvaloniaView : ITextInputMethodImpl
             case TextInputContentType.Url:
                 KeyboardType = UIKeyboardType.Url;
                 break;
-            
+
             case TextInputContentType.Name:
                 KeyboardType = UIKeyboardType.NamePhonePad;
                 break;
-            
+
             case TextInputContentType.Password:
                 KeyboardType = UIKeyboardType.Default;
                 IsSecureEntry = true;
                 break;
-            
+
             case TextInputContentType.Social:
                 KeyboardType = UIKeyboardType.Twitter;
                 break;
-                
+
             case TextInputContentType.Search:
                 KeyboardType = UIKeyboardType.WebSearch;
                 break;
@@ -148,8 +139,335 @@ public partial class AvaloniaView : ITextInputMethodImpl
         }
     }
 
+
     void ITextInputMethodImpl.Reset()
     {
         ResignFirstResponder();
     }
+
+    // Traits (Optional)
+    [Export("keyboardType")] public UIKeyboardType KeyboardType { get; private set; } = UIKeyboardType.Default;
+
+    [Export("isSecureTextEntry")] public bool IsSecureEntry { get; private set; }
+
+    [Export("returnKeyType")] public UIReturnKeyType ReturnKeyType { get; set; }
+
+    void IUIKeyInput.InsertText(string text)
+    {
+        if (_client == null)
+        {
+            return;
+        }
+
+        if (text == "\n")
+        {
+            // emulate return key released.
+        }
+
+        switch (ReturnKeyType)
+        {
+            case UIReturnKeyType.Done:
+            case UIReturnKeyType.Search:
+            case UIReturnKeyType.Go:
+            case UIReturnKeyType.Send:
+                ResignFirstResponder();
+                return;
+        }
+
+        // TODO replace this with _client.SetCommitText?
+        if (KeyboardDevice.Instance is { })
+        {
+            _topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance,
+                0, InputRoot, text));
+        }
+    }
+
+    void IUIKeyInput.DeleteBackward()
+    {
+        if (KeyboardDevice.Instance is { })
+        {
+            // TODO: pass this through IME infrastructure instead of emulating a backspace press
+            _topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance,
+                0, InputRoot, RawKeyEventType.KeyDown, Key.Back, RawInputModifiers.None));
+
+            _topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance,
+                0, InputRoot, RawKeyEventType.KeyUp, Key.Back, RawInputModifiers.None));
+        }
+    }
+
+    bool IUIKeyInput.HasText => true;
+
+    string IUITextInput.TextInRange(UITextRange range)
+    {
+        var text = _client.SurroundingText.Text;
+
+        if (!string.IsNullOrWhiteSpace(_markedText))
+        {
+            // todo check this combining _marked text with surrounding text.
+            int cursorPos = _client.SurroundingText.CursorOffset;
+            text = text[.. cursorPos] + _markedText + text[cursorPos ..];
+        }
+
+        var start = (range.Start as AvaloniaTextPosition).Offset;
+        int end = (range.End as AvaloniaTextPosition).Offset;
+
+        return text[start .. end];
+    }
+
+    void IUITextInput.ReplaceText(UITextRange range, string text)
+    {
+        ((IUITextInput)this).SelectedTextRange = range;
+
+        // todo _client.SetCommitText(text);
+        if (KeyboardDevice.Instance is { })
+        {
+            _topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance,
+                0, InputRoot, text));
+        }
+    }
+
+    void IUITextInput.SetMarkedText(string markedText, NSRange selectedRange)
+    {
+        _markedText = markedText;
+
+        // todo check this... seems correct
+        _client.SetPreeditText(markedText);
+    }
+
+    void IUITextInput.UnmarkText()
+    {
+        if (string.IsNullOrWhiteSpace(_markedText))
+            return;
+
+        // todo _client.CommitString (_markedText);
+
+        _markedText = "";
+    }
+
+    UITextRange IUITextInput.GetTextRange(UITextPosition fromPosition, UITextPosition toPosition)
+    {
+        if (fromPosition is AvaloniaTextPosition f && toPosition is AvaloniaTextPosition t)
+        {
+            // todo check calculation.
+            return new AvaloniaTextRange(f.Offset, t.Offset);
+        }
+
+        throw new Exception();
+    }
+
+    UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, nint offset)
+    {
+        if (fromPosition is AvaloniaTextPosition f)
+        {
+            var position = f.Offset;
+            int posPlusIndex = position + (int)offset;
+            var length = _client.SurroundingText.Text.Length;
+
+            if (posPlusIndex < 0 || posPlusIndex > length)
+            {
+                return null;
+            }
+
+            return new AvaloniaTextPosition(posPlusIndex);
+        }
+
+        throw new Exception();
+    }
+
+    UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, UITextLayoutDirection inDirection, nint offset)
+    {
+        if (fromPosition is AvaloniaTextPosition f)
+        {
+            var pos = f.Offset;
+
+            switch (inDirection)
+            {
+                case UITextLayoutDirection.Left:
+                    return new AvaloniaTextPosition(pos - (int)offset);
+
+                case UITextLayoutDirection.Right:
+                    return new AvaloniaTextPosition(pos + (int)offset);
+
+                default:
+                    return fromPosition;
+            }
+        }
+
+        throw new Exception();
+    }
+
+    NSComparisonResult IUITextInput.ComparePosition(UITextPosition first, UITextPosition second)
+    {
+        if (first is AvaloniaTextPosition f && second is AvaloniaTextPosition s)
+        {
+            if (f.Offset > s.Offset)
+                return NSComparisonResult.Ascending;
+
+            if (f.Offset < s.Offset)
+                return NSComparisonResult.Descending;
+
+            return NSComparisonResult.Same;
+        }
+
+        throw new Exception();
+    }
+
+    nint IUITextInput.GetOffsetFromPosition(UITextPosition fromPosition, UITextPosition toPosition)
+    {
+        if (fromPosition is AvaloniaTextPosition f && toPosition is AvaloniaTextPosition t)
+        {
+            return t.Offset - f.Offset;
+        }
+
+        throw new Exception();
+    }
+
+    UITextPosition IUITextInput.GetPositionWithinRange(UITextRange range, UITextLayoutDirection direction)
+    {
+        if (range is AvaloniaTextRange r)
+        {
+            switch (direction)
+            {
+                case UITextLayoutDirection.Right:
+                    return r.End;
+
+                default:
+                    return r.Start;
+            }
+        }
+
+        throw new Exception();
+    }
+
+    UITextRange IUITextInput.GetCharacterRange(UITextPosition byExtendingPosition, UITextLayoutDirection direction)
+    {
+        if (byExtendingPosition is AvaloniaTextPosition p)
+        {
+            switch (direction)
+            {
+                case UITextLayoutDirection.Left:
+                    return new AvaloniaTextRange(0, p.Offset);
+
+                default:
+                    // todo check this.
+                    return new AvaloniaTextRange(p.Offset, _client.SurroundingText.Text.Length);
+            }
+        }
+
+        throw new Exception();
+    }
+
+    NSWritingDirection IUITextInput.GetBaseWritingDirection(UITextPosition forPosition,
+        UITextStorageDirection direction)
+    {
+        return NSWritingDirection.LeftToRight;
+
+        // todo query and retyrn RTL.
+    }
+
+    void IUITextInput.SetBaseWritingDirectionforRange(NSWritingDirection writingDirection, UITextRange range)
+    {
+        // todo ? ignore?
+    }
+
+    CGRect IUITextInput.GetFirstRectForRange(UITextRange range)
+    {
+        if (_client == null)
+            return CGRect.Empty;
+
+        if (!string.IsNullOrWhiteSpace(_markedText))
+        {
+            return CGRect.Empty;
+        }
+
+        if (range is AvaloniaTextRange r)
+        {
+            // todo add ime apis to get cursor rect.
+            throw new NotImplementedException();
+        }
+
+        throw new Exception();
+    }
+
+    CGRect IUITextInput.GetCaretRectForPosition(UITextPosition? position)
+    {
+        var rect = _client.CursorRectangle;
+
+        return new CGRect(rect.X, rect.Y, rect.Width, rect.Height);
+    }
+
+    UITextPosition IUITextInput.GetClosestPositionToPoint(CGPoint point)
+    {
+        // TODO HitTest text? 
+        throw new System.NotImplementedException();
+    }
+
+    UITextPosition IUITextInput.GetClosestPositionToPoint(CGPoint point, UITextRange withinRange)
+    {
+        // TODO HitTest text? 
+        throw new System.NotImplementedException();
+    }
+
+    UITextRange IUITextInput.GetCharacterRangeAtPoint(CGPoint point)
+    {
+        // TODO check if needed, hittest?
+        return new AvaloniaTextRange(_client.SurroundingText.CursorOffset, _client.SurroundingText.CursorOffset);
+    }
+
+    UITextSelectionRect[] IUITextInput.GetSelectionRects(UITextRange range)
+    {
+        // todo?
+        return Array.Empty<UITextSelectionRect>();
+    }
+
+    UITextRange? IUITextInput.SelectedTextRange
+    {
+        get
+        {
+            return new AvaloniaTextRange(
+                Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset),
+                Math.Max(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset));
+        }
+        set
+        {
+            throw new NotImplementedException();
+        }
+    }
+
+    NSDictionary? IUITextInput.MarkedTextStyle
+    {
+        get => _markedTextStyle;
+        set => _markedTextStyle = value;
+    }
+
+    UITextPosition IUITextInput.BeginningOfDocument => _beginningOfDocument;
+
+    UITextPosition IUITextInput.EndOfDocument
+    {
+        get
+        {
+            return new AvaloniaTextPosition(_client.SurroundingText.Text.Length + _markedText.Length);
+        }
+    }
+
+    NSObject? IUITextInput.WeakInputDelegate
+    {
+        get => _inputDelegate as TextInputHandler;
+        set => throw new NotSupportedException();
+    }
+
+    NSObject IUITextInput.WeakTokenizer => _tokenizer;
+
+    UITextRange IUITextInput.MarkedTextRange
+    {
+        get
+        {
+            if (string.IsNullOrWhiteSpace(_markedText))
+            {
+                return null;
+            }
+
+            return new AvaloniaTextRange(0, _markedText.Length);            
+        }
+    }
 }

+ 1 - 0
src/iOS/Avalonia.iOS/AvaloniaView.cs

@@ -32,6 +32,7 @@ namespace Avalonia.iOS
             _touches = new TouchHandler(this, _topLevelImpl);
             _topLevel = new EmbeddableControlRoot(_topLevelImpl);
             _inputDelegate = new TextInputHandler();
+            _tokenizer = new UITextInputStringTokenizer(this);
             
             _topLevel.Prepare();