13 changed files with 655 additions and 599 deletions
@ -1,500 +1,54 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
using Foundation; |
|||
using ObjCRuntime; |
|||
#nullable enable |
|||
using Avalonia.Input.TextInput; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
using CoreGraphics; |
|||
using JetBrains.Annotations; |
|||
using UIKit; |
|||
|
|||
namespace Avalonia.iOS; |
|||
|
|||
#nullable enable |
|||
|
|||
[Adopts("UITextInput")] |
|||
[Adopts("UITextInputTraits")] |
|||
[Adopts("UIKeyInput")] |
|||
public class AvaloniaResponder : UIResponder, IUITextInput |
|||
public partial class AvaloniaView |
|||
{ |
|||
private class AvaloniaTextRange : UITextRange |
|||
{ |
|||
private readonly NSRange _range; |
|||
|
|||
public AvaloniaTextRange(NSRange range) |
|||
{ |
|||
_range = range; |
|||
} |
|||
|
|||
public override AvaloniaTextPosition Start => new AvaloniaTextPosition((int)_range.Location); |
|||
|
|||
public override AvaloniaTextPosition End => new AvaloniaTextPosition((int)(_range.Location + _range.Length)); |
|||
|
|||
public NSRange Range => _range; |
|||
|
|||
public override bool IsEmpty => _range.Length == 0; |
|||
} |
|||
|
|||
private class AvaloniaTextPosition : UITextPosition |
|||
{ |
|||
public AvaloniaTextPosition(nint index) |
|||
{ |
|||
Index = index; |
|||
} |
|||
|
|||
public nint Index { get; } |
|||
|
|||
//[Export("inputDelegate")]
|
|||
//public UITextInputDelegate InputDelegate { get; set; }
|
|||
|
|||
[Export("foo")] |
|||
public void Foo() |
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
private UIResponder _nextResponder; |
|||
|
|||
public AvaloniaResponder(AvaloniaView view, ITextInputMethodClient client) |
|||
{ |
|||
_nextResponder = view; |
|||
_client = client; |
|||
_tokenizer = new UITextInputStringTokenizer(this); |
|||
|
|||
} |
|||
|
|||
public override UIResponder NextResponder => _nextResponder; |
|||
|
|||
public override bool CanPerform(Selector action, NSObject? withSender) |
|||
{ |
|||
return base.CanPerform(action, withSender); |
|||
} |
|||
|
|||
private string _markedText = ""; |
|||
private ITextInputMethodClient? _client; |
|||
private NSDictionary? _markedTextStyle; |
|||
private readonly UITextPosition _beginningOfDocument = new AvaloniaTextPosition(0); |
|||
private readonly UITextInputStringTokenizer _tokenizer; |
|||
|
|||
public ITextInputMethodClient? Client => _client; |
|||
|
|||
public bool IsActive => _client != null; |
|||
|
|||
public override bool CanResignFirstResponder => true; |
|||
|
|||
public override bool CanBecomeFirstResponder => true; |
|||
|
|||
public override UIEditingInteractionConfiguration EditingInteractionConfiguration => |
|||
UIEditingInteractionConfiguration.Default; |
|||
|
|||
public override NSString TextInputContextIdentifier => new NSString("Test"); |
|||
|
|||
public override UITextInputMode TextInputMode => UITextInputMode.CurrentInputMode; |
|||
|
|||
[DllImport("/usr/lib/libobjc.dylib")] |
|||
extern static void objc_msgSend(IntPtr receiver, IntPtr selector, IntPtr arg); |
|||
|
|||
private static readonly IntPtr SelectionWillChange = Selector.GetHandle("selectionWillChange:"); |
|||
private static readonly IntPtr SelectionDidChange = Selector.GetHandle("selectionDidChange:"); |
|||
private static readonly IntPtr TextWillChange = Selector.GetHandle("textWillChange:"); |
|||
private static readonly IntPtr TextDidChange = Selector.GetHandle("textDidChange:"); |
|||
|
|||
private void ClientOnCursorRectangleChanged(object? sender, EventArgs e) |
|||
{ |
|||
objc_msgSend(WeakInputDelegate.Handle.Handle, SelectionWillChange, this.Handle.Handle); |
|||
objc_msgSend(WeakInputDelegate.Handle.Handle, SelectionDidChange, this.Handle.Handle); |
|||
} |
|||
|
|||
private void ClientOnSurroundingTextChanged(object? sender, EventArgs e) |
|||
{ |
|||
objc_msgSend(WeakInputDelegate.Handle.Handle, TextWillChange, Handle.Handle); |
|||
objc_msgSend(WeakInputDelegate.Handle.Handle, TextDidChange, Handle.Handle); |
|||
} |
|||
|
|||
// Traits (Optional)
|
|||
[Export("autocapitalizationType")] public UITextAutocapitalizationType AutocapitalizationType { get; private set; } |
|||
|
|||
[Export("autocorrectionType")] public UITextAutocorrectionType AutocorrectionType { get; private set; } |
|||
[Export("keyboardType")] public UIKeyboardType KeyboardType { get; private set; } = UIKeyboardType.Default; |
|||
|
|||
[Export("keyboardAppearance")] |
|||
public UIKeyboardAppearance KeyboardAppearance { get; private set; } = UIKeyboardAppearance.Default; |
|||
|
|||
[Export("returnKeyType")] public UIReturnKeyType ReturnKeyType { get; set; } |
|||
|
|||
[Export("enablesReturnKeyAutomatically")] public bool EnablesReturnKeyAutomatically { get; set; } |
|||
[Export("isSecureTextEntry")] public bool IsSecureEntry { get; private set; } |
|||
|
|||
[Export("spellCheckingType")] |
|||
public UITextSpellCheckingType SpellCheckingType { get; set; } = UITextSpellCheckingType.Default; |
|||
|
|||
[Export("textContentType")] |
|||
public NSString TextContentType { get; set; } |
|||
|
|||
[Export("smartQuotesType")] |
|||
public UITextSmartQuotesType SmartQuotesType { get; set; } = UITextSmartQuotesType.Default; |
|||
|
|||
[Export("smartDashesType")] |
|||
public UITextSmartDashesType SmartDashesType { get; set; } = UITextSmartDashesType.Default; |
|||
|
|||
[Export("smartInsertDeleteType")] |
|||
public UITextSmartInsertDeleteType SmartInsertDeleteType { get; set; } = UITextSmartInsertDeleteType.Default; |
|||
|
|||
[Export("passwordRules")] |
|||
public UITextInputPasswordRules PasswordRules { 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 = (int)(range.Start as AvaloniaTextPosition).Index; |
|||
var end = (int)(range.End as AvaloniaTextPosition).Index; |
|||
|
|||
var result = text[start .. end]; |
|||
|
|||
return result; |
|||
} |
|||
|
|||
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 = ""; |
|||
} |
|||
|
|||
public UITextRange GetTextRange(UITextPosition fromPosition, UITextPosition toPosition) |
|||
{ |
|||
if (fromPosition is AvaloniaTextPosition f && toPosition is AvaloniaTextPosition t) |
|||
{ |
|||
// todo check calculation.
|
|||
var range = new NSRange(Math.Min(f.Index, t.Index), Math.Abs(t.Index - f.Index)); |
|||
return new AvaloniaTextRange(range); |
|||
} |
|||
|
|||
return null; |
|||
//throw new Exception();
|
|||
} |
|||
|
|||
UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, nint offset) |
|||
{ |
|||
if (fromPosition is AvaloniaTextPosition indexedPosition) |
|||
{ |
|||
var end = indexedPosition.Index + offset; |
|||
// Verify position is valid in document.
|
|||
//if (end > self.text.length || end < 0) {
|
|||
// return nil;
|
|||
//}
|
|||
|
|||
return new AvaloniaTextPosition(end); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, UITextLayoutDirection inDirection, nint offset) |
|||
{ |
|||
if (fromPosition is AvaloniaTextPosition f) |
|||
{ |
|||
var newPosition = f.Index; |
|||
|
|||
switch (inDirection) |
|||
{ |
|||
case UITextLayoutDirection.Left: |
|||
newPosition -= offset; |
|||
break; |
|||
|
|||
case UITextLayoutDirection.Right: |
|||
newPosition += offset; |
|||
break; |
|||
} |
|||
|
|||
if (newPosition < 0) |
|||
{ |
|||
newPosition = 0; |
|||
} |
|||
|
|||
if (newPosition > _client.SurroundingText.Text.Length) |
|||
{ |
|||
newPosition = _client.SurroundingText.Text.Length; |
|||
} |
|||
|
|||
return new AvaloniaTextPosition(newPosition); |
|||
} |
|||
|
|||
throw new Exception(); |
|||
} |
|||
|
|||
NSComparisonResult IUITextInput.ComparePosition(UITextPosition first, UITextPosition second) |
|||
{ |
|||
if (first is AvaloniaTextPosition f && second is AvaloniaTextPosition s) |
|||
{ |
|||
if (f.Index < s.Index) |
|||
return NSComparisonResult.Ascending; |
|||
|
|||
if (f.Index > s.Index) |
|||
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.Index - f.Index; |
|||
} |
|||
|
|||
throw new Exception(); |
|||
} |
|||
|
|||
UITextPosition IUITextInput.GetPositionWithinRange(UITextRange range, UITextLayoutDirection direction) |
|||
{ |
|||
if (range is AvaloniaTextRange indexedRange) |
|||
{ |
|||
nint position = 0; |
|||
|
|||
switch (direction) |
|||
{ |
|||
case UITextLayoutDirection.Up: |
|||
case UITextLayoutDirection.Left: |
|||
position = indexedRange.Range.Location; |
|||
break; |
|||
|
|||
case UITextLayoutDirection.Down: |
|||
case UITextLayoutDirection.Right: |
|||
position = indexedRange.Range.Location + indexedRange.Range.Length; |
|||
break; |
|||
} |
|||
|
|||
return new AvaloniaTextPosition(position); |
|||
} |
|||
|
|||
throw new Exception(); |
|||
} |
|||
|
|||
UITextRange IUITextInput.GetCharacterRange(UITextPosition byExtendingPosition, UITextLayoutDirection direction) |
|||
{ |
|||
if (byExtendingPosition is AvaloniaTextPosition pos) |
|||
{ |
|||
NSRange result = new NSRange(); |
|||
|
|||
switch (direction) |
|||
{ |
|||
case UITextLayoutDirection.Up: |
|||
case UITextLayoutDirection.Left: |
|||
result = new NSRange(pos.Index - 1, 1); |
|||
break; |
|||
|
|||
case UITextLayoutDirection.Right: |
|||
case UITextLayoutDirection.Down: |
|||
result = new NSRange(pos.Index, 1); |
|||
break; |
|||
} |
|||
|
|||
return new AvaloniaTextRange(result); |
|||
} |
|||
private const string ImeLog = "IOSIME"; |
|||
private Rect _cursorRect; |
|||
private TextInputOptions? _options; |
|||
|
|||
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) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
UITextSelectionRect[] IUITextInput.GetSelectionRects(UITextRange range) |
|||
private static UIResponder? CurrentAvaloniaResponder { get; set; } |
|||
public override bool BecomeFirstResponder() |
|||
{ |
|||
return null; |
|||
var res = base.BecomeFirstResponder(); |
|||
if (res) |
|||
CurrentAvaloniaResponder = this; |
|||
return res; |
|||
} |
|||
|
|||
[Export("textStylingAtPosition:inDirection:")] |
|||
public NSDictionary GetTextStylingAtPosition(UITextPosition position, UITextStorageDirection direction) |
|||
public override bool ResignFirstResponder() |
|||
{ |
|||
return null; |
|||
var res = base.ResignFirstResponder(); |
|||
if (res && ReferenceEquals(CurrentAvaloniaResponder, this)) |
|||
CurrentAvaloniaResponder = null; |
|||
return res; |
|||
} |
|||
|
|||
UITextRange? IUITextInput.SelectedTextRange |
|||
{ |
|||
get |
|||
{ |
|||
return new AvaloniaTextRange(new NSRange( |
|||
(nint)Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset), |
|||
(nint)Math.Abs(_client.SurroundingText.CursorOffset - _client.SurroundingText.AnchorOffset))); |
|||
} |
|||
set |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
private bool IsDrivingText => CurrentAvaloniaResponder is TextInputResponder t && ReferenceEquals(t.NextResponder, this); |
|||
|
|||
NSDictionary? IUITextInput.MarkedTextStyle |
|||
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) |
|||
{ |
|||
get => _markedTextStyle; |
|||
set => _markedTextStyle = value; |
|||
} |
|||
|
|||
UITextPosition IUITextInput.BeginningOfDocument => _beginningOfDocument; |
|||
_client = client; |
|||
if (_client == null && IsDrivingText) |
|||
BecomeFirstResponder(); |
|||
|
|||
UITextPosition IUITextInput.EndOfDocument |
|||
{ |
|||
get |
|||
if (_client is { }) |
|||
{ |
|||
return new AvaloniaTextPosition(_client.SurroundingText.Text.Length + _markedText.Length); |
|||
new TextInputResponder(this, _client).BecomeFirstResponder(); |
|||
} |
|||
} |
|||
|
|||
void ITextInputMethodImpl.SetCursorRect(Rect rect) => _cursorRect = rect; |
|||
|
|||
public NSObject? WeakInputDelegate |
|||
{ |
|||
get; |
|||
set; |
|||
} |
|||
|
|||
NSObject IUITextInput.WeakTokenizer => _tokenizer; |
|||
void ITextInputMethodImpl.SetOptions(TextInputOptions options) => _options = options; |
|||
|
|||
UITextRange IUITextInput.MarkedTextRange |
|||
void ITextInputMethodImpl.Reset() |
|||
{ |
|||
get |
|||
{ |
|||
if (_client == null || string.IsNullOrWhiteSpace(_markedText)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
// todo
|
|||
return new AvaloniaTextRange(new NSRange( |
|||
(nint)Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset), |
|||
(nint)Math.Abs(_client.SurroundingText.CursorOffset - _client.SurroundingText.AnchorOffset))); |
|||
} |
|||
if (IsDrivingText) |
|||
BecomeFirstResponder(); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,40 @@ |
|||
using System; |
|||
using Avalonia.Controls.Documents; |
|||
|
|||
namespace Avalonia.iOS; |
|||
|
|||
internal ref struct CombinedSpan3<T> |
|||
{ |
|||
public ReadOnlySpan<T> Span1, Span2, Span3; |
|||
|
|||
public CombinedSpan3(ReadOnlySpan<T> span1, ReadOnlySpan<T> span2, ReadOnlySpan<T> span3) |
|||
{ |
|||
Span1 = span1; |
|||
Span2 = span2; |
|||
Span3 = span3; |
|||
} |
|||
|
|||
public int Length => Span1.Length + Span2.Length + Span3.Length; |
|||
|
|||
void CopyFromSpan(ReadOnlySpan<T> from, ref int offset, ref Span<T> to) |
|||
{ |
|||
if(to.Length == 0) |
|||
return; |
|||
if (offset < from.Length) |
|||
{ |
|||
var copyNow = Math.Min(from.Length - offset, to.Length); |
|||
from.Slice(offset, copyNow).CopyTo(to); |
|||
to = to.Slice(copyNow); |
|||
offset = 0; |
|||
} |
|||
else |
|||
offset -= from.Length; |
|||
} |
|||
|
|||
public void CopyTo(Span<T> to, int offset) |
|||
{ |
|||
CopyFromSpan(Span1, ref offset, ref to); |
|||
CopyFromSpan(Span2, ref offset, ref to); |
|||
CopyFromSpan(Span3, ref offset, ref to); |
|||
} |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
#nullable enable |
|||
using Avalonia.Input.TextInput; |
|||
using Foundation; |
|||
using UIKit; |
|||
|
|||
namespace Avalonia.iOS; |
|||
|
|||
partial class AvaloniaView |
|||
{ |
|||
partial class TextInputResponder |
|||
{ |
|||
[Export("autocapitalizationType")] |
|||
public UITextAutocapitalizationType AutocapitalizationType { get; private set; } |
|||
|
|||
[Export("autocorrectionType")] |
|||
public UITextAutocorrectionType AutocorrectionType => UITextAutocorrectionType.Yes; |
|||
|
|||
[Export("keyboardType")] |
|||
public UIKeyboardType KeyboardType => |
|||
_view._options == null ? |
|||
UIKeyboardType.Default : |
|||
_view._options.ContentType switch |
|||
{ |
|||
TextInputContentType.Alpha => UIKeyboardType.AsciiCapable, |
|||
TextInputContentType.Digits or TextInputContentType.Number => UIKeyboardType.NumberPad, |
|||
TextInputContentType.Pin => UIKeyboardType.NumberPad, |
|||
TextInputContentType.Email => UIKeyboardType.EmailAddress, |
|||
TextInputContentType.Url => UIKeyboardType.Url, |
|||
TextInputContentType.Name => UIKeyboardType.NamePhonePad, |
|||
TextInputContentType.Social => UIKeyboardType.Twitter, |
|||
TextInputContentType.Search => UIKeyboardType.WebSearch, |
|||
_ => UIKeyboardType.Default |
|||
}; |
|||
|
|||
[Export("keyboardAppearance")] |
|||
public UIKeyboardAppearance KeyboardAppearance => UIKeyboardAppearance.Alert; |
|||
|
|||
[Export("returnKeyType")] public UIReturnKeyType ReturnKeyType => UIReturnKeyType.Default; |
|||
|
|||
[Export("enablesReturnKeyAutomatically")] |
|||
public bool EnablesReturnKeyAutomatically { get; set; } |
|||
|
|||
[Export("isSecureTextEntry")] public bool IsSecureEntry => |
|||
_view._options?.ContentType is TextInputContentType.Password or TextInputContentType.Pin |
|||
|| (_view._options?.IsSensitive ?? false); |
|||
|
|||
[Export("spellCheckingType")] public UITextSpellCheckingType SpellCheckingType => UITextSpellCheckingType.Yes; |
|||
|
|||
[Export("textContentType")] public NSString TextContentType { get; set; } = new NSString("text/plain"); |
|||
|
|||
[Export("smartQuotesType")] |
|||
public UITextSmartQuotesType SmartQuotesType { get; set; } = UITextSmartQuotesType.Default; |
|||
|
|||
[Export("smartDashesType")] |
|||
public UITextSmartDashesType SmartDashesType { get; set; } = UITextSmartDashesType.Default; |
|||
|
|||
[Export("smartInsertDeleteType")] |
|||
public UITextSmartInsertDeleteType SmartInsertDeleteType { get; set; } = UITextSmartInsertDeleteType.Default; |
|||
|
|||
[Export("passwordRules")] public UITextInputPasswordRules PasswordRules { get; set; } = null!; |
|||
|
|||
public NSObject? WeakInputDelegate |
|||
{ |
|||
get; |
|||
set; |
|||
} |
|||
|
|||
NSObject IUITextInput.WeakTokenizer => _tokenizer; |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,471 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
using System.Runtime.Versioning; |
|||
using Foundation; |
|||
using ObjCRuntime; |
|||
using Avalonia.Input.TextInput; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
using Avalonia.Logging; |
|||
using CoreGraphics; |
|||
using UIKit; |
|||
// ReSharper disable InconsistentNaming
|
|||
// ReSharper disable StringLiteralTypo
|
|||
|
|||
namespace Avalonia.iOS; |
|||
|
|||
#nullable enable |
|||
|
|||
partial class AvaloniaView |
|||
{ |
|||
|
|||
[Adopts("UITextInput")] |
|||
[Adopts("UITextInputTraits")] |
|||
[Adopts("UIKeyInput")] |
|||
partial class TextInputResponder : UIResponder, IUITextInput |
|||
{ |
|||
private class AvaloniaTextRange : UITextRange, INSCopying |
|||
{ |
|||
private UITextPosition? _start; |
|||
private UITextPosition? _end; |
|||
public int StartIndex { get; } |
|||
public int EndIndex { get; } |
|||
|
|||
public AvaloniaTextRange(int startIndex, int endIndex) |
|||
{ |
|||
if (startIndex < 0) |
|||
throw new ArgumentOutOfRangeException(nameof(startIndex)); |
|||
|
|||
if (endIndex < startIndex) |
|||
throw new ArgumentOutOfRangeException(nameof(endIndex)); |
|||
|
|||
StartIndex = startIndex; |
|||
EndIndex = endIndex; |
|||
} |
|||
|
|||
public override bool IsEmpty => StartIndex == EndIndex; |
|||
|
|||
public override UITextPosition Start => _start ??= new AvaloniaTextPosition(StartIndex); |
|||
public override UITextPosition End => _end ??= new AvaloniaTextPosition(EndIndex); |
|||
public NSObject Copy(NSZone? zone) |
|||
{ |
|||
return new AvaloniaTextRange(StartIndex, EndIndex); |
|||
} |
|||
} |
|||
|
|||
private class AvaloniaTextPosition : UITextPosition, INSCopying |
|||
{ |
|||
public AvaloniaTextPosition(int index) |
|||
{ |
|||
if (index < 0) |
|||
throw new ArgumentOutOfRangeException(nameof(index)); |
|||
Index = index; |
|||
} |
|||
|
|||
public int Index { get; } |
|||
public NSObject Copy(NSZone? zone) => new AvaloniaTextPosition(Index); |
|||
} |
|||
|
|||
public TextInputResponder(AvaloniaView view, ITextInputMethodClient client) |
|||
{ |
|||
_view = view; |
|||
NextResponder = view; |
|||
_client = client; |
|||
_tokenizer = new UITextInputStringTokenizer(this); |
|||
} |
|||
|
|||
public override UIResponder NextResponder { get; } |
|||
|
|||
private readonly ITextInputMethodClient _client; |
|||
private int _inSurroundingTextUpdateEvent; |
|||
private readonly UITextPosition _beginningOfDocument = new AvaloniaTextPosition(0); |
|||
private readonly UITextInputStringTokenizer _tokenizer; |
|||
|
|||
public ITextInputMethodClient? Client => _client; |
|||
|
|||
public override bool CanResignFirstResponder => true; |
|||
|
|||
public override bool CanBecomeFirstResponder => true; |
|||
|
|||
public override UIEditingInteractionConfiguration EditingInteractionConfiguration => |
|||
UIEditingInteractionConfiguration.Default; |
|||
|
|||
public override NSString TextInputContextIdentifier => new NSString(Guid.NewGuid().ToString()); |
|||
|
|||
public override UITextInputMode TextInputMode => UITextInputMode.CurrentInputMode; |
|||
|
|||
[DllImport("/usr/lib/libobjc.dylib")] |
|||
private static extern void objc_msgSend(IntPtr receiver, IntPtr selector, IntPtr arg); |
|||
|
|||
private static readonly IntPtr SelectionWillChange = Selector.GetHandle("selectionWillChange:"); |
|||
private static readonly IntPtr SelectionDidChange = Selector.GetHandle("selectionDidChange:"); |
|||
private static readonly IntPtr TextWillChange = Selector.GetHandle("textWillChange:"); |
|||
private static readonly IntPtr TextDidChange = Selector.GetHandle("textDidChange:"); |
|||
private readonly AvaloniaView _view; |
|||
private string? _markedText; |
|||
|
|||
|
|||
|
|||
private void SurroundingTextChanged(object? sender, EventArgs e) |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "SurroundingTextChanged"); |
|||
if (WeakInputDelegate == null) |
|||
return; |
|||
_inSurroundingTextUpdateEvent++; |
|||
try |
|||
{ |
|||
objc_msgSend(WeakInputDelegate.Handle.Handle, TextWillChange, Handle.Handle); |
|||
objc_msgSend(WeakInputDelegate.Handle.Handle, TextDidChange, Handle.Handle); |
|||
objc_msgSend(WeakInputDelegate.Handle.Handle, SelectionWillChange, this.Handle.Handle); |
|||
objc_msgSend(WeakInputDelegate.Handle.Handle, SelectionDidChange, this.Handle.Handle); |
|||
} |
|||
finally |
|||
{ |
|||
_inSurroundingTextUpdateEvent--; |
|||
} |
|||
} |
|||
|
|||
private void KeyPress(Key ev) |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "Triggering key press {key}", ev); |
|||
_view._topLevelImpl.Input(new RawKeyEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot, |
|||
RawKeyEventType.KeyDown, ev, RawInputModifiers.None)); |
|||
|
|||
_view._topLevelImpl.Input(new RawKeyEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot, |
|||
RawKeyEventType.KeyUp, ev, RawInputModifiers.None)); |
|||
} |
|||
|
|||
private void TextInput(string text) |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "Triggering text input {text}", text); |
|||
_view._topLevelImpl.Input(new RawTextInputEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot, text)); |
|||
} |
|||
|
|||
void IUIKeyInput.InsertText(string text) |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.InsertText {text}", text); |
|||
|
|||
if (text == "\n") |
|||
{ |
|||
KeyPress(Key.Enter); |
|||
return; |
|||
} |
|||
|
|||
TextInput(text); |
|||
} |
|||
|
|||
void IUIKeyInput.DeleteBackward() => KeyPress(Key.Back); |
|||
|
|||
bool IUIKeyInput.HasText => true; |
|||
|
|||
string IUITextInput.TextInRange(UITextRange range) |
|||
{ |
|||
var r = (AvaloniaTextRange)range; |
|||
var s = _client.SurroundingText; |
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.TextInRange {start} {end}", r.StartIndex, r.EndIndex); |
|||
|
|||
string result = ""; |
|||
if(string.IsNullOrEmpty(_markedText)) |
|||
result = s.Text[r.StartIndex .. r.EndIndex]; |
|||
else |
|||
{ |
|||
var span = new CombinedSpan3<char>(s.Text.AsSpan().Slice(0, s.CursorOffset), |
|||
_markedText, |
|||
s.Text.AsSpan().Slice(s.CursorOffset)); |
|||
var buf = new char[r.EndIndex - r.StartIndex]; |
|||
span.CopyTo(buf, r.StartIndex); |
|||
result = new string(buf); |
|||
} |
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "result: {res}", result); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
void IUITextInput.ReplaceText(UITextRange range, string text) |
|||
{ |
|||
var r = (AvaloniaTextRange)range; |
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)? |
|||
.Log(null, "IUIKeyInput.ReplaceText {start} {end} {text}", r.StartIndex, r.EndIndex, text); |
|||
_client.SelectInSurroundingText(r.StartIndex, r.EndIndex); |
|||
TextInput(text); |
|||
} |
|||
|
|||
void IUITextInput.SetMarkedText(string markedText, NSRange selectedRange) |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)? |
|||
.Log(null, "IUIKeyInput.SetMarkedText {start} {len} {text}", selectedRange.Location, |
|||
selectedRange.Location, markedText); |
|||
|
|||
_markedText = markedText; |
|||
_client.SetPreeditText(markedText); |
|||
} |
|||
|
|||
void IUITextInput.UnmarkText() |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.UnmarkText"); |
|||
if(_markedText == null) |
|||
return; |
|||
var commitString = _markedText; |
|||
_markedText = null; |
|||
_client.SetPreeditText(null); |
|||
if (string.IsNullOrWhiteSpace(commitString)) |
|||
return; |
|||
TextInput(commitString); |
|||
} |
|||
|
|||
public UITextRange GetTextRange(UITextPosition fromPosition, UITextPosition toPosition) |
|||
{ |
|||
var f = (AvaloniaTextPosition)fromPosition; |
|||
var t = (AvaloniaTextPosition)toPosition; |
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.GetTextRange {start} {end}", f.Index, t.Index); |
|||
|
|||
return new AvaloniaTextRange(f.Index, t.Index); |
|||
} |
|||
|
|||
UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, nint offset) |
|||
{ |
|||
var pos = (AvaloniaTextPosition)fromPosition; |
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog) |
|||
?.Log(null, "IUIKeyInput.GetPosition {start} {offset}", pos.Index, (int)offset); |
|||
|
|||
var res = GetPositionCore(pos, offset); |
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog) |
|||
?.Log(null, $"res: " + (res == null ? "null" : (int)res.Index)); |
|||
return res!; |
|||
} |
|||
|
|||
private AvaloniaTextPosition? GetPositionCore(AvaloniaTextPosition pos, nint offset) |
|||
{ |
|||
|
|||
var end = pos.Index + (int)offset; |
|||
if (end < 0) |
|||
return null!; |
|||
if (end > DocumentLength) |
|||
return null; |
|||
return new AvaloniaTextPosition(end); |
|||
} |
|||
|
|||
UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, UITextLayoutDirection inDirection, |
|||
nint offset) |
|||
{ |
|||
var pos = (AvaloniaTextPosition)fromPosition; |
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog) |
|||
?.Log(null, "IUIKeyInput.GetPosition {start} {direction} {offset}", pos.Index, inDirection, (int)offset); |
|||
|
|||
var res = GetPositionCore(pos, inDirection, offset); |
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog) |
|||
?.Log(null, $"res: " + (res == null ? "null" : (int)res.Index)); |
|||
return res!; |
|||
} |
|||
|
|||
private AvaloniaTextPosition? GetPositionCore(AvaloniaTextPosition fromPosition, UITextLayoutDirection inDirection, |
|||
nint offset) |
|||
{ |
|||
var f = (AvaloniaTextPosition)fromPosition; |
|||
var newPosition = f.Index; |
|||
|
|||
switch (inDirection) |
|||
{ |
|||
case UITextLayoutDirection.Left: |
|||
newPosition -= (int)offset; |
|||
break; |
|||
|
|||
case UITextLayoutDirection.Right: |
|||
newPosition += (int)offset; |
|||
break; |
|||
} |
|||
|
|||
if (newPosition < 0) |
|||
return null!; |
|||
|
|||
if (newPosition > DocumentLength) |
|||
return null!; |
|||
|
|||
return new AvaloniaTextPosition(newPosition); |
|||
} |
|||
|
|||
NSComparisonResult IUITextInput.ComparePosition(UITextPosition first, UITextPosition second) |
|||
{ |
|||
var f = (AvaloniaTextPosition)first; |
|||
var s = (AvaloniaTextPosition)second; |
|||
if (f.Index < s.Index) |
|||
return NSComparisonResult.Ascending; |
|||
|
|||
if (f.Index > s.Index) |
|||
return NSComparisonResult.Descending; |
|||
|
|||
return NSComparisonResult.Same; |
|||
} |
|||
|
|||
nint IUITextInput.GetOffsetFromPosition(UITextPosition fromPosition, UITextPosition toPosition) |
|||
{ |
|||
var f = (AvaloniaTextPosition)fromPosition; |
|||
var t = (AvaloniaTextPosition)toPosition; |
|||
return t.Index - f.Index; |
|||
} |
|||
|
|||
UITextPosition IUITextInput.GetPositionWithinRange(UITextRange range, UITextLayoutDirection direction) |
|||
{ |
|||
var r = (AvaloniaTextRange)range; |
|||
|
|||
if (direction is UITextLayoutDirection.Right or UITextLayoutDirection.Down) |
|||
return r.End; |
|||
return r.Start; |
|||
} |
|||
|
|||
UITextRange IUITextInput.GetCharacterRange(UITextPosition byExtendingPosition, UITextLayoutDirection direction) |
|||
{ |
|||
var p = (AvaloniaTextPosition)byExtendingPosition; |
|||
if (direction is UITextLayoutDirection.Left or UITextLayoutDirection.Up) |
|||
return new AvaloniaTextRange(0, p.Index); |
|||
|
|||
return new AvaloniaTextRange(p.Index, DocumentLength); |
|||
} |
|||
|
|||
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) |
|||
{ |
|||
|
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)? |
|||
.Log(null, "IUITextInput:GetFirstRectForRange"); |
|||
// TODO: Query from the input client
|
|||
var r = _view._cursorRect; |
|||
|
|||
return new CGRect(r.Left, r.Top, r.Width, r.Height); |
|||
} |
|||
|
|||
CGRect IUITextInput.GetCaretRectForPosition(UITextPosition? position) |
|||
{ |
|||
// TODO: Query from the input client
|
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)? |
|||
.Log(null, "IUITextInput:GetCaretRectForPosition"); |
|||
var rect = _client.CursorRectangle; |
|||
|
|||
return new CGRect(rect.X, rect.Y, rect.Width, rect.Height); |
|||
} |
|||
|
|||
UITextPosition IUITextInput.GetClosestPositionToPoint(CGPoint point) |
|||
{ |
|||
// TODO: Query from the input client
|
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)? |
|||
.Log(null, "IUITextInput:GetClosestPositionToPoint"); |
|||
return new AvaloniaTextPosition(0); |
|||
} |
|||
|
|||
UITextPosition IUITextInput.GetClosestPositionToPoint(CGPoint point, UITextRange withinRange) |
|||
{ |
|||
// TODO: Query from the input client
|
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)? |
|||
.Log(null, "IUITextInput:GetClosestPositionToPoint"); |
|||
return new AvaloniaTextPosition(0); |
|||
} |
|||
|
|||
UITextRange IUITextInput.GetCharacterRangeAtPoint(CGPoint point) |
|||
{ |
|||
// TODO: Query from the input client
|
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)? |
|||
.Log(null, "IUITextInput:GetCharacterRangeAtPoint"); |
|||
return new AvaloniaTextRange(0, 0); |
|||
} |
|||
|
|||
UITextSelectionRect[] IUITextInput.GetSelectionRects(UITextRange range) |
|||
{ |
|||
// TODO: Query from the input client
|
|||
Logger.TryGet(LogEventLevel.Debug, ImeLog)? |
|||
.Log(null, "IUITextInput:GetSelectionRect"); |
|||
return new UITextSelectionRect[0]; |
|||
} |
|||
|
|||
[Export("textStylingAtPosition:inDirection:")] |
|||
public NSDictionary GetTextStylingAtPosition(UITextPosition position, UITextStorageDirection direction) |
|||
{ |
|||
return null!; |
|||
} |
|||
|
|||
UITextRange? IUITextInput.SelectedTextRange |
|||
{ |
|||
get |
|||
{ |
|||
return new AvaloniaTextRange( |
|||
Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset), |
|||
Math.Max(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset)); |
|||
} |
|||
set |
|||
{ |
|||
if (_inSurroundingTextUpdateEvent > 0) |
|||
return; |
|||
if (value == null) |
|||
_client.SelectInSurroundingText(_client.SurroundingText.CursorOffset, |
|||
_client.SurroundingText.CursorOffset); |
|||
else |
|||
{ |
|||
var r = (AvaloniaTextRange)value; |
|||
_client.SelectInSurroundingText(r.StartIndex, r.EndIndex); |
|||
} |
|||
} |
|||
} |
|||
|
|||
NSDictionary? IUITextInput.MarkedTextStyle |
|||
{ |
|||
get => null; |
|||
set {} |
|||
} |
|||
|
|||
UITextPosition IUITextInput.BeginningOfDocument => _beginningOfDocument; |
|||
|
|||
private int DocumentLength => _client.SurroundingText.Text.Length + (_markedText?.Length ?? 0); |
|||
UITextPosition IUITextInput.EndOfDocument => new AvaloniaTextPosition(DocumentLength); |
|||
|
|||
UITextRange IUITextInput.MarkedTextRange |
|||
{ |
|||
get |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(_markedText)) |
|||
return null!; |
|||
return new AvaloniaTextRange(_client.SurroundingText.CursorOffset, _client.SurroundingText.CursorOffset + _markedText.Length); |
|||
} |
|||
} |
|||
|
|||
public override bool BecomeFirstResponder() |
|||
{ |
|||
var res = base.BecomeFirstResponder(); |
|||
if (res) |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Debug, "IOSIME") |
|||
?.Log(null, "Became first responder"); |
|||
_client.SurroundingTextChanged += SurroundingTextChanged; |
|||
CurrentAvaloniaResponder = this; |
|||
} |
|||
|
|||
return res; |
|||
} |
|||
|
|||
|
|||
public override bool ResignFirstResponder() |
|||
{ |
|||
var res = base.ResignFirstResponder(); |
|||
if (res && ReferenceEquals(CurrentAvaloniaResponder, this)) |
|||
{ |
|||
|
|||
Logger.TryGet(LogEventLevel.Debug, "IOSIME") |
|||
?.Log(null, "Resigned first responder"); |
|||
_client.SurroundingTextChanged -= SurroundingTextChanged; |
|||
CurrentAvaloniaResponder = null; |
|||
} |
|||
|
|||
return res; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue