123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- using System;
- using System.Runtime.InteropServices;
- using Avalonia.Controls.Presenters;
- using Avalonia.Input;
- using Avalonia.Input.Raw;
- using Avalonia.Input.TextInput;
- using Avalonia.Logging;
- using CoreGraphics;
- using Foundation;
- using ObjCRuntime;
- 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 static AvaloniaEmptyTextPosition? _emptyPosition;
- private static AvaloniaEmptyTextPosition EmptyPosition => _emptyPosition ??= new();
- 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);
- }
- private class AvaloniaEmptyTextPosition : UITextPosition, INSCopying
- {
- public AvaloniaEmptyTextPosition()
- {
- }
- public NSObject Copy(NSZone? zone) => this;
- }
- public TextInputResponder(AvaloniaView view, TextInputMethodClient client)
- {
- _view = view;
- NextResponder = view;
- _client = client;
- _tokenizer = new UITextInputStringTokenizer(this);
- }
- public override UIResponder NextResponder { get; }
- private readonly TextInputMethodClient _client;
- private int _inSurroundingTextUpdateEvent;
- private readonly UITextPosition _beginningOfDocument = new AvaloniaTextPosition(0);
- private readonly UITextInputStringTokenizer _tokenizer;
- public TextInputMethodClient? 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
- {
- get
- {
- var mode = UITextInputMode.CurrentInputMode;
- // Can be empty see https://developer.apple.com/documentation/uikit/uitextinputmode/1614522-activeinputmodes
- if (mode is null && UITextInputMode.ActiveInputModes.Length > 0)
- {
- mode = UITextInputMode.ActiveInputModes[0];
- }
- // See: https://stackoverflow.com/a/33337483/20894223
- if (mode is null)
- {
- using var tv = new UITextView();
- mode = tv.TextInputMode;
- }
- return mode;
- }
- }
- [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 key, PhysicalKey physicalKey, string? keySymbol)
- {
- Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "Triggering key press {key}", key);
- if (_view._topLevelImpl.Input is { } input)
- {
- input.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot,
- RawKeyEventType.KeyDown, key, RawInputModifiers.None, physicalKey, keySymbol));
- input.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot,
- RawKeyEventType.KeyUp, key, RawInputModifiers.None, physicalKey, keySymbol));
- }
- }
- private void TextInput(string text)
- {
- Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "Triggering text input {text}", text);
- _view._topLevelImpl.Input?.Invoke(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, PhysicalKey.Enter, "\r");
- switch (ReturnKeyType)
- {
- case UIReturnKeyType.Next:
- FocusManager.GetFocusManager(_view._topLevel)?
- .TryMoveFocus(NavigationDirection.Next);
- break;
- case UIReturnKeyType.Done:
- case UIReturnKeyType.Go:
- case UIReturnKeyType.Send:
- case UIReturnKeyType.Search:
- ResignFirstResponder();
- break;
- }
- return;
- }
- TextInput(text);
- }
- void IUIKeyInput.DeleteBackward() => KeyPress(Key.Back, PhysicalKey.Backspace, "\b");
- bool IUIKeyInput.HasText => true;
- string IUITextInput.TextInRange(UITextRange range)
- {
- var r = (AvaloniaTextRange)range;
- var surroundingText = _client.SurroundingText;
- var currentSelection = _client.Selection;
- Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.TextInRange {start} {end}", r.StartIndex, r.EndIndex);
- string result = "";
- if (string.IsNullOrEmpty(_markedText))
- if(surroundingText != null && r.EndIndex < surroundingText.Length)
- {
- result = surroundingText[r.StartIndex..r.EndIndex];
- }
- else
- {
- var span = new CombinedSpan3<char>(surroundingText.AsSpan().Slice(0, currentSelection.Start),
- _markedText,
- surroundingText.AsSpan().Slice(currentSelection.Start));
- 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.Selection = new TextSelection(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)
- {
- Logger.TryGet(LogEventLevel.Debug, ImeLog)?
- .Log(null, "IUITextInput:GetClosestPositionToPoint");
- var presenter = _client.TextViewVisual as TextPresenter;
- if (presenter is { })
- {
- var hitResult = presenter.TextLayout.HitTestPoint(new Point(point.X, point.Y));
- return new AvaloniaTextPosition(hitResult.TextPosition);
- }
- return EmptyPosition;
- }
- 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 Array.Empty<UITextSelectionRect>();
- }
- [Export("textStylingAtPosition:inDirection:")]
- public NSDictionary GetTextStylingAtPosition(UITextPosition position, UITextStorageDirection direction)
- {
- return null!;
- }
- UITextRange? IUITextInput.SelectedTextRange
- {
- get
- {
- return new AvaloniaTextRange(_client.Selection.Start, _client.Selection.End);
- }
- set
- {
- if (_inSurroundingTextUpdateEvent > 0)
- return;
- if (value == null)
- _client.Selection = default;
- else
- {
- var r = (AvaloniaTextRange)value;
- _client.Selection = new TextSelection(r.StartIndex, r.EndIndex);
- }
- }
- }
- NSDictionary? IUITextInput.MarkedTextStyle
- {
- get => null;
- set { }
- }
- UITextPosition IUITextInput.BeginningOfDocument => _beginningOfDocument;
- private int DocumentLength => (_client.SurroundingText?.Length ?? 0) + (_markedText?.Length ?? 0);
- UITextPosition IUITextInput.EndOfDocument => new AvaloniaTextPosition(DocumentLength);
- UITextRange IUITextInput.MarkedTextRange
- {
- get
- {
- if (string.IsNullOrWhiteSpace(_markedText))
- return null!;
- return new AvaloniaTextRange(_client.Selection.Start, _client.Selection.Start + _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;
- }
- }
- }
|