TextInputResponder.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. using System;
  2. using System.Runtime.InteropServices;
  3. using Avalonia.Controls.Presenters;
  4. using Avalonia.Input;
  5. using Avalonia.Input.Raw;
  6. using Avalonia.Input.TextInput;
  7. using Avalonia.Logging;
  8. using CoreGraphics;
  9. using Foundation;
  10. using ObjCRuntime;
  11. using UIKit;
  12. // ReSharper disable InconsistentNaming
  13. // ReSharper disable StringLiteralTypo
  14. namespace Avalonia.iOS;
  15. #nullable enable
  16. partial class AvaloniaView
  17. {
  18. [Adopts("UITextInput")]
  19. [Adopts("UITextInputTraits")]
  20. [Adopts("UIKeyInput")]
  21. partial class TextInputResponder : UIResponder, IUITextInput
  22. {
  23. private static AvaloniaEmptyTextPosition? _emptyPosition;
  24. private static AvaloniaEmptyTextPosition EmptyPosition => _emptyPosition ??= new();
  25. private class AvaloniaTextRange : UITextRange, INSCopying
  26. {
  27. private UITextPosition? _start;
  28. private UITextPosition? _end;
  29. public int StartIndex { get; }
  30. public int EndIndex { get; }
  31. public AvaloniaTextRange(int startIndex, int endIndex)
  32. {
  33. if (startIndex < 0)
  34. throw new ArgumentOutOfRangeException(nameof(startIndex));
  35. if (endIndex < startIndex)
  36. throw new ArgumentOutOfRangeException(nameof(endIndex));
  37. StartIndex = startIndex;
  38. EndIndex = endIndex;
  39. }
  40. public override bool IsEmpty => StartIndex == EndIndex;
  41. public override UITextPosition Start => _start ??= new AvaloniaTextPosition(StartIndex);
  42. public override UITextPosition End => _end ??= new AvaloniaTextPosition(EndIndex);
  43. public NSObject Copy(NSZone? zone)
  44. {
  45. return new AvaloniaTextRange(StartIndex, EndIndex);
  46. }
  47. }
  48. private class AvaloniaTextPosition : UITextPosition, INSCopying
  49. {
  50. public AvaloniaTextPosition(int index)
  51. {
  52. if (index < 0)
  53. throw new ArgumentOutOfRangeException(nameof(index));
  54. Index = index;
  55. }
  56. public int Index { get; }
  57. public NSObject Copy(NSZone? zone) => new AvaloniaTextPosition(Index);
  58. }
  59. private class AvaloniaEmptyTextPosition : UITextPosition, INSCopying
  60. {
  61. public AvaloniaEmptyTextPosition()
  62. {
  63. }
  64. public NSObject Copy(NSZone? zone) => this;
  65. }
  66. public TextInputResponder(AvaloniaView view, TextInputMethodClient client)
  67. {
  68. _view = view;
  69. NextResponder = view;
  70. _client = client;
  71. _tokenizer = new UITextInputStringTokenizer(this);
  72. }
  73. public override UIResponder NextResponder { get; }
  74. private readonly TextInputMethodClient _client;
  75. private int _inSurroundingTextUpdateEvent;
  76. private readonly UITextPosition _beginningOfDocument = new AvaloniaTextPosition(0);
  77. private readonly UITextInputStringTokenizer _tokenizer;
  78. public TextInputMethodClient? Client => _client;
  79. public override bool CanResignFirstResponder => true;
  80. public override bool CanBecomeFirstResponder => true;
  81. public override UIEditingInteractionConfiguration EditingInteractionConfiguration =>
  82. UIEditingInteractionConfiguration.Default;
  83. public override NSString TextInputContextIdentifier => new NSString(Guid.NewGuid().ToString());
  84. public override UITextInputMode TextInputMode
  85. {
  86. get
  87. {
  88. var mode = UITextInputMode.CurrentInputMode;
  89. // Can be empty see https://developer.apple.com/documentation/uikit/uitextinputmode/1614522-activeinputmodes
  90. if (mode is null && UITextInputMode.ActiveInputModes.Length > 0)
  91. {
  92. mode = UITextInputMode.ActiveInputModes[0];
  93. }
  94. // See: https://stackoverflow.com/a/33337483/20894223
  95. if (mode is null)
  96. {
  97. using var tv = new UITextView();
  98. mode = tv.TextInputMode;
  99. }
  100. return mode;
  101. }
  102. }
  103. [DllImport("/usr/lib/libobjc.dylib")]
  104. private static extern void objc_msgSend(IntPtr receiver, IntPtr selector, IntPtr arg);
  105. private static readonly IntPtr SelectionWillChange = Selector.GetHandle("selectionWillChange:");
  106. private static readonly IntPtr SelectionDidChange = Selector.GetHandle("selectionDidChange:");
  107. private static readonly IntPtr TextWillChange = Selector.GetHandle("textWillChange:");
  108. private static readonly IntPtr TextDidChange = Selector.GetHandle("textDidChange:");
  109. private readonly AvaloniaView _view;
  110. private string? _markedText;
  111. private void SurroundingTextChanged(object? sender, EventArgs e)
  112. {
  113. Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "SurroundingTextChanged");
  114. if (WeakInputDelegate == null)
  115. return;
  116. _inSurroundingTextUpdateEvent++;
  117. try
  118. {
  119. objc_msgSend(WeakInputDelegate.Handle.Handle, TextWillChange, Handle.Handle);
  120. objc_msgSend(WeakInputDelegate.Handle.Handle, TextDidChange, Handle.Handle);
  121. objc_msgSend(WeakInputDelegate.Handle.Handle, SelectionWillChange, this.Handle.Handle);
  122. objc_msgSend(WeakInputDelegate.Handle.Handle, SelectionDidChange, this.Handle.Handle);
  123. }
  124. finally
  125. {
  126. _inSurroundingTextUpdateEvent--;
  127. }
  128. }
  129. private void KeyPress(Key key, PhysicalKey physicalKey, string? keySymbol)
  130. {
  131. Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "Triggering key press {key}", key);
  132. if (_view._topLevelImpl.Input is { } input)
  133. {
  134. input.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot,
  135. RawKeyEventType.KeyDown, key, RawInputModifiers.None, physicalKey, keySymbol));
  136. input.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot,
  137. RawKeyEventType.KeyUp, key, RawInputModifiers.None, physicalKey, keySymbol));
  138. }
  139. }
  140. private void TextInput(string text)
  141. {
  142. Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "Triggering text input {text}", text);
  143. _view._topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot, text));
  144. }
  145. void IUIKeyInput.InsertText(string text)
  146. {
  147. Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.InsertText {text}", text);
  148. if (text == "\n")
  149. {
  150. KeyPress(Key.Enter, PhysicalKey.Enter, "\r");
  151. switch (ReturnKeyType)
  152. {
  153. case UIReturnKeyType.Next:
  154. FocusManager.GetFocusManager(_view._topLevel)?
  155. .TryMoveFocus(NavigationDirection.Next);
  156. break;
  157. case UIReturnKeyType.Done:
  158. case UIReturnKeyType.Go:
  159. case UIReturnKeyType.Send:
  160. case UIReturnKeyType.Search:
  161. ResignFirstResponder();
  162. break;
  163. }
  164. return;
  165. }
  166. TextInput(text);
  167. }
  168. void IUIKeyInput.DeleteBackward() => KeyPress(Key.Back, PhysicalKey.Backspace, "\b");
  169. bool IUIKeyInput.HasText => true;
  170. string IUITextInput.TextInRange(UITextRange range)
  171. {
  172. var r = (AvaloniaTextRange)range;
  173. var surroundingText = _client.SurroundingText;
  174. var currentSelection = _client.Selection;
  175. Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.TextInRange {start} {end}", r.StartIndex, r.EndIndex);
  176. string result = "";
  177. if (string.IsNullOrEmpty(_markedText))
  178. if(surroundingText != null && r.EndIndex < surroundingText.Length)
  179. {
  180. result = surroundingText[r.StartIndex..r.EndIndex];
  181. }
  182. else
  183. {
  184. var span = new CombinedSpan3<char>(surroundingText.AsSpan().Slice(0, currentSelection.Start),
  185. _markedText,
  186. surroundingText.AsSpan().Slice(currentSelection.Start));
  187. var buf = new char[r.EndIndex - r.StartIndex];
  188. span.CopyTo(buf, r.StartIndex);
  189. result = new string(buf);
  190. }
  191. Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "result: {res}", result);
  192. return result;
  193. }
  194. void IUITextInput.ReplaceText(UITextRange range, string text)
  195. {
  196. var r = (AvaloniaTextRange)range;
  197. Logger.TryGet(LogEventLevel.Debug, ImeLog)?
  198. .Log(null, "IUIKeyInput.ReplaceText {start} {end} {text}", r.StartIndex, r.EndIndex, text);
  199. _client.Selection = new TextSelection(r.StartIndex, r.EndIndex);
  200. TextInput(text);
  201. }
  202. void IUITextInput.SetMarkedText(string markedText, NSRange selectedRange)
  203. {
  204. Logger.TryGet(LogEventLevel.Debug, ImeLog)?
  205. .Log(null, "IUIKeyInput.SetMarkedText {start} {len} {text}", selectedRange.Location,
  206. selectedRange.Location, markedText);
  207. _markedText = markedText;
  208. _client.SetPreeditText(markedText);
  209. }
  210. void IUITextInput.UnmarkText()
  211. {
  212. Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.UnmarkText");
  213. if (_markedText == null)
  214. return;
  215. var commitString = _markedText;
  216. _markedText = null;
  217. _client.SetPreeditText(null);
  218. if (string.IsNullOrWhiteSpace(commitString))
  219. return;
  220. TextInput(commitString);
  221. }
  222. public UITextRange GetTextRange(UITextPosition fromPosition, UITextPosition toPosition)
  223. {
  224. var f = (AvaloniaTextPosition)fromPosition;
  225. var t = (AvaloniaTextPosition)toPosition;
  226. Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.GetTextRange {start} {end}", f.Index, t.Index);
  227. return new AvaloniaTextRange(f.Index, t.Index);
  228. }
  229. UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, nint offset)
  230. {
  231. var pos = (AvaloniaTextPosition)fromPosition;
  232. Logger.TryGet(LogEventLevel.Debug, ImeLog)
  233. ?.Log(null, "IUIKeyInput.GetPosition {start} {offset}", pos.Index, (int)offset);
  234. var res = GetPositionCore(pos, offset);
  235. Logger.TryGet(LogEventLevel.Debug, ImeLog)
  236. ?.Log(null, $"res: " + (res == null ? "null" : (int)res.Index));
  237. return res!;
  238. }
  239. private AvaloniaTextPosition? GetPositionCore(AvaloniaTextPosition pos, nint offset)
  240. {
  241. var end = pos.Index + (int)offset;
  242. if (end < 0)
  243. return null!;
  244. if (end > DocumentLength)
  245. return null;
  246. return new AvaloniaTextPosition(end);
  247. }
  248. UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, UITextLayoutDirection inDirection,
  249. nint offset)
  250. {
  251. var pos = (AvaloniaTextPosition)fromPosition;
  252. Logger.TryGet(LogEventLevel.Debug, ImeLog)
  253. ?.Log(null, "IUIKeyInput.GetPosition {start} {direction} {offset}", pos.Index, inDirection, (int)offset);
  254. var res = GetPositionCore(pos, inDirection, offset);
  255. Logger.TryGet(LogEventLevel.Debug, ImeLog)
  256. ?.Log(null, $"res: " + (res == null ? "null" : (int)res.Index));
  257. return res!;
  258. }
  259. private AvaloniaTextPosition? GetPositionCore(AvaloniaTextPosition fromPosition, UITextLayoutDirection inDirection,
  260. nint offset)
  261. {
  262. var f = (AvaloniaTextPosition)fromPosition;
  263. var newPosition = f.Index;
  264. switch (inDirection)
  265. {
  266. case UITextLayoutDirection.Left:
  267. newPosition -= (int)offset;
  268. break;
  269. case UITextLayoutDirection.Right:
  270. newPosition += (int)offset;
  271. break;
  272. }
  273. if (newPosition < 0)
  274. return null!;
  275. if (newPosition > DocumentLength)
  276. return null!;
  277. return new AvaloniaTextPosition(newPosition);
  278. }
  279. NSComparisonResult IUITextInput.ComparePosition(UITextPosition first, UITextPosition second)
  280. {
  281. var f = (AvaloniaTextPosition)first;
  282. var s = (AvaloniaTextPosition)second;
  283. if (f.Index < s.Index)
  284. return NSComparisonResult.Ascending;
  285. if (f.Index > s.Index)
  286. return NSComparisonResult.Descending;
  287. return NSComparisonResult.Same;
  288. }
  289. nint IUITextInput.GetOffsetFromPosition(UITextPosition fromPosition, UITextPosition toPosition)
  290. {
  291. var f = (AvaloniaTextPosition)fromPosition;
  292. var t = (AvaloniaTextPosition)toPosition;
  293. return t.Index - f.Index;
  294. }
  295. UITextPosition IUITextInput.GetPositionWithinRange(UITextRange range, UITextLayoutDirection direction)
  296. {
  297. var r = (AvaloniaTextRange)range;
  298. if (direction is UITextLayoutDirection.Right or UITextLayoutDirection.Down)
  299. return r.End;
  300. return r.Start;
  301. }
  302. UITextRange IUITextInput.GetCharacterRange(UITextPosition byExtendingPosition, UITextLayoutDirection direction)
  303. {
  304. var p = (AvaloniaTextPosition)byExtendingPosition;
  305. if (direction is UITextLayoutDirection.Left or UITextLayoutDirection.Up)
  306. return new AvaloniaTextRange(0, p.Index);
  307. return new AvaloniaTextRange(p.Index, DocumentLength);
  308. }
  309. NSWritingDirection IUITextInput.GetBaseWritingDirection(UITextPosition forPosition,
  310. UITextStorageDirection direction)
  311. {
  312. return NSWritingDirection.LeftToRight;
  313. // todo query and retyrn RTL.
  314. }
  315. void IUITextInput.SetBaseWritingDirectionforRange(NSWritingDirection writingDirection, UITextRange range)
  316. {
  317. // todo ? ignore?
  318. }
  319. CGRect IUITextInput.GetFirstRectForRange(UITextRange range)
  320. {
  321. Logger.TryGet(LogEventLevel.Debug, ImeLog)?
  322. .Log(null, "IUITextInput:GetFirstRectForRange");
  323. // TODO: Query from the input client
  324. var r = _view._cursorRect;
  325. return new CGRect(r.Left, r.Top, r.Width, r.Height);
  326. }
  327. CGRect IUITextInput.GetCaretRectForPosition(UITextPosition? position)
  328. {
  329. // TODO: Query from the input client
  330. Logger.TryGet(LogEventLevel.Debug, ImeLog)?
  331. .Log(null, "IUITextInput:GetCaretRectForPosition");
  332. var rect = _client.CursorRectangle;
  333. return new CGRect(rect.X, rect.Y, rect.Width, rect.Height);
  334. }
  335. UITextPosition IUITextInput.GetClosestPositionToPoint(CGPoint point)
  336. {
  337. Logger.TryGet(LogEventLevel.Debug, ImeLog)?
  338. .Log(null, "IUITextInput:GetClosestPositionToPoint");
  339. var presenter = _client.TextViewVisual as TextPresenter;
  340. if (presenter is { })
  341. {
  342. var hitResult = presenter.TextLayout.HitTestPoint(new Point(point.X, point.Y));
  343. return new AvaloniaTextPosition(hitResult.TextPosition);
  344. }
  345. return EmptyPosition;
  346. }
  347. UITextPosition IUITextInput.GetClosestPositionToPoint(CGPoint point, UITextRange withinRange)
  348. {
  349. // TODO: Query from the input client
  350. Logger.TryGet(LogEventLevel.Debug, ImeLog)?
  351. .Log(null, "IUITextInput:GetClosestPositionToPoint");
  352. return new AvaloniaTextPosition(0);
  353. }
  354. UITextRange IUITextInput.GetCharacterRangeAtPoint(CGPoint point)
  355. {
  356. // TODO: Query from the input client
  357. Logger.TryGet(LogEventLevel.Debug, ImeLog)?
  358. .Log(null, "IUITextInput:GetCharacterRangeAtPoint");
  359. return new AvaloniaTextRange(0, 0);
  360. }
  361. UITextSelectionRect[] IUITextInput.GetSelectionRects(UITextRange range)
  362. {
  363. // TODO: Query from the input client
  364. Logger.TryGet(LogEventLevel.Debug, ImeLog)?
  365. .Log(null, "IUITextInput:GetSelectionRect");
  366. return Array.Empty<UITextSelectionRect>();
  367. }
  368. [Export("textStylingAtPosition:inDirection:")]
  369. public NSDictionary GetTextStylingAtPosition(UITextPosition position, UITextStorageDirection direction)
  370. {
  371. return null!;
  372. }
  373. UITextRange? IUITextInput.SelectedTextRange
  374. {
  375. get
  376. {
  377. return new AvaloniaTextRange(_client.Selection.Start, _client.Selection.End);
  378. }
  379. set
  380. {
  381. if (_inSurroundingTextUpdateEvent > 0)
  382. return;
  383. if (value == null)
  384. _client.Selection = default;
  385. else
  386. {
  387. var r = (AvaloniaTextRange)value;
  388. _client.Selection = new TextSelection(r.StartIndex, r.EndIndex);
  389. }
  390. }
  391. }
  392. NSDictionary? IUITextInput.MarkedTextStyle
  393. {
  394. get => null;
  395. set { }
  396. }
  397. UITextPosition IUITextInput.BeginningOfDocument => _beginningOfDocument;
  398. private int DocumentLength => (_client.SurroundingText?.Length ?? 0) + (_markedText?.Length ?? 0);
  399. UITextPosition IUITextInput.EndOfDocument => new AvaloniaTextPosition(DocumentLength);
  400. UITextRange IUITextInput.MarkedTextRange
  401. {
  402. get
  403. {
  404. if (string.IsNullOrWhiteSpace(_markedText))
  405. return null!;
  406. return new AvaloniaTextRange(_client.Selection.Start, _client.Selection.Start + _markedText.Length);
  407. }
  408. }
  409. public override bool BecomeFirstResponder()
  410. {
  411. var res = base.BecomeFirstResponder();
  412. if (res)
  413. {
  414. Logger.TryGet(LogEventLevel.Debug, "IOSIME")
  415. ?.Log(null, "Became first responder");
  416. _client.SurroundingTextChanged += SurroundingTextChanged;
  417. CurrentAvaloniaResponder = this;
  418. }
  419. return res;
  420. }
  421. public override bool ResignFirstResponder()
  422. {
  423. var res = base.ResignFirstResponder();
  424. if (res && ReferenceEquals(CurrentAvaloniaResponder, this))
  425. {
  426. Logger.TryGet(LogEventLevel.Debug, "IOSIME")
  427. ?.Log(null, "Resigned first responder");
  428. _client.SurroundingTextChanged -= SurroundingTextChanged;
  429. CurrentAvaloniaResponder = null;
  430. }
  431. return res;
  432. }
  433. }
  434. }