Flutter iOS Embedder
FlutterTextInputPlugin.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
7 
8 #import <Foundation/Foundation.h>
9 #import <UIKit/UIKit.h>
10 
11 #include "unicode/uchar.h"
12 
13 #include "flutter/fml/logging.h"
14 #include "flutter/fml/platform/darwin/string_range_sanitization.h"
16 
18 
19 static const char kTextAffinityDownstream[] = "TextAffinity.downstream";
20 static const char kTextAffinityUpstream[] = "TextAffinity.upstream";
21 // A delay before enabling the accessibility of FlutterTextInputView after
22 // it is activated.
23 static constexpr double kUITextInputAccessibilityEnablingDelaySeconds = 0.5;
24 
25 // A delay before reenabling the UIView areAnimationsEnabled to YES
26 // in order for becomeFirstResponder to receive the proper value.
27 static const NSTimeInterval kKeyboardAnimationDelaySeconds = 0.1;
28 
29 // A time set for the screenshot to animate back to the assigned position.
30 static const NSTimeInterval kKeyboardAnimationTimeToCompleteion = 0.3;
31 
32 // The "canonical" invalid CGRect, similar to CGRectNull, used to
33 // indicate a CGRect involved in firstRectForRange calculation is
34 // invalid. The specific value is chosen so that if firstRectForRange
35 // returns kInvalidFirstRect, iOS will not show the IME candidates view.
36 const CGRect kInvalidFirstRect = {{-1, -1}, {9999, 9999}};
37 
38 #pragma mark - TextInput channel method names.
39 // See https://api.flutter.dev/flutter/services/SystemChannels/textInput-constant.html
40 static NSString* const kShowMethod = @"TextInput.show";
41 static NSString* const kHideMethod = @"TextInput.hide";
42 static NSString* const kSetClientMethod = @"TextInput.setClient";
43 static NSString* const kSetPlatformViewClientMethod = @"TextInput.setPlatformViewClient";
44 static NSString* const kSetEditingStateMethod = @"TextInput.setEditingState";
45 static NSString* const kClearClientMethod = @"TextInput.clearClient";
46 static NSString* const kSetEditableSizeAndTransformMethod =
47  @"TextInput.setEditableSizeAndTransform";
48 static NSString* const kSetMarkedTextRectMethod = @"TextInput.setMarkedTextRect";
49 static NSString* const kFinishAutofillContextMethod = @"TextInput.finishAutofillContext";
50 // TODO(justinmc): Remove the TextInput method constant when the framework has
51 // finished transitioning to using the Scribble channel.
52 // https://github.com/flutter/flutter/pull/104128
53 static NSString* const kDeprecatedSetSelectionRectsMethod = @"TextInput.setSelectionRects";
54 static NSString* const kSetSelectionRectsMethod = @"Scribble.setSelectionRects";
55 static NSString* const kStartLiveTextInputMethod = @"TextInput.startLiveTextInput";
56 static NSString* const kUpdateConfigMethod = @"TextInput.updateConfig";
58  @"TextInput.onPointerMoveForInteractiveKeyboard";
59 static NSString* const kOnInteractiveKeyboardPointerUpMethod =
60  @"TextInput.onPointerUpForInteractiveKeyboard";
61 
62 #pragma mark - TextInputConfiguration Field Names
63 static NSString* const kSecureTextEntry = @"obscureText";
64 static NSString* const kKeyboardType = @"inputType";
65 static NSString* const kKeyboardAppearance = @"keyboardAppearance";
66 static NSString* const kInputAction = @"inputAction";
67 static NSString* const kEnableDeltaModel = @"enableDeltaModel";
68 static NSString* const kEnableInteractiveSelection = @"enableInteractiveSelection";
69 
70 static NSString* const kSmartDashesType = @"smartDashesType";
71 static NSString* const kSmartQuotesType = @"smartQuotesType";
72 
73 static NSString* const kAssociatedAutofillFields = @"fields";
74 
75 // TextInputConfiguration.autofill and sub-field names
76 static NSString* const kAutofillProperties = @"autofill";
77 static NSString* const kAutofillId = @"uniqueIdentifier";
78 static NSString* const kAutofillEditingValue = @"editingValue";
79 static NSString* const kAutofillHints = @"hints";
80 
81 static NSString* const kAutocorrectionType = @"autocorrect";
82 
83 #pragma mark - Static Functions
84 
85 // Determine if the character at `range` of `text` is an emoji.
86 static BOOL IsEmoji(NSString* text, NSRange charRange) {
87  UChar32 codePoint;
88  BOOL gotCodePoint = [text getBytes:&codePoint
89  maxLength:sizeof(codePoint)
90  usedLength:NULL
91  encoding:NSUTF32StringEncoding
92  options:kNilOptions
93  range:charRange
94  remainingRange:NULL];
95  return gotCodePoint && u_hasBinaryProperty(codePoint, UCHAR_EMOJI);
96 }
97 
98 // "TextInputType.none" is a made-up input type that's typically
99 // used when there's an in-app virtual keyboard. If
100 // "TextInputType.none" is specified, disable the system
101 // keyboard.
102 static BOOL ShouldShowSystemKeyboard(NSDictionary* type) {
103  NSString* inputType = type[@"name"];
104  return ![inputType isEqualToString:@"TextInputType.none"];
105 }
106 static UIKeyboardType ToUIKeyboardType(NSDictionary* type) {
107  NSString* inputType = type[@"name"];
108  if ([inputType isEqualToString:@"TextInputType.address"]) {
109  return UIKeyboardTypeDefault;
110  }
111  if ([inputType isEqualToString:@"TextInputType.datetime"]) {
112  return UIKeyboardTypeNumbersAndPunctuation;
113  }
114  if ([inputType isEqualToString:@"TextInputType.emailAddress"]) {
115  return UIKeyboardTypeEmailAddress;
116  }
117  if ([inputType isEqualToString:@"TextInputType.multiline"]) {
118  return UIKeyboardTypeDefault;
119  }
120  if ([inputType isEqualToString:@"TextInputType.name"]) {
121  return UIKeyboardTypeNamePhonePad;
122  }
123  if ([inputType isEqualToString:@"TextInputType.number"]) {
124  if ([type[@"signed"] boolValue]) {
125  return UIKeyboardTypeNumbersAndPunctuation;
126  }
127  if ([type[@"decimal"] boolValue]) {
128  return UIKeyboardTypeDecimalPad;
129  }
130  return UIKeyboardTypeNumberPad;
131  }
132  if ([inputType isEqualToString:@"TextInputType.phone"]) {
133  return UIKeyboardTypePhonePad;
134  }
135  if ([inputType isEqualToString:@"TextInputType.text"]) {
136  return UIKeyboardTypeDefault;
137  }
138  if ([inputType isEqualToString:@"TextInputType.url"]) {
139  return UIKeyboardTypeURL;
140  }
141  if ([inputType isEqualToString:@"TextInputType.visiblePassword"]) {
142  return UIKeyboardTypeASCIICapable;
143  }
144  if ([inputType isEqualToString:@"TextInputType.webSearch"]) {
145  return UIKeyboardTypeWebSearch;
146  }
147  if ([inputType isEqualToString:@"TextInputType.twitter"]) {
148  return UIKeyboardTypeTwitter;
149  }
150  return UIKeyboardTypeDefault;
151 }
152 
153 static UITextAutocapitalizationType ToUITextAutoCapitalizationType(NSDictionary* type) {
154  NSString* textCapitalization = type[@"textCapitalization"];
155  if ([textCapitalization isEqualToString:@"TextCapitalization.characters"]) {
156  return UITextAutocapitalizationTypeAllCharacters;
157  } else if ([textCapitalization isEqualToString:@"TextCapitalization.sentences"]) {
158  return UITextAutocapitalizationTypeSentences;
159  } else if ([textCapitalization isEqualToString:@"TextCapitalization.words"]) {
160  return UITextAutocapitalizationTypeWords;
161  }
162  return UITextAutocapitalizationTypeNone;
163 }
164 
165 static UIReturnKeyType ToUIReturnKeyType(NSString* inputType) {
166  // Where did the term "unspecified" come from? iOS has a "default" and Android
167  // has "unspecified." These 2 terms seem to mean the same thing but we need
168  // to pick just one. "unspecified" was chosen because "default" is often a
169  // reserved word in languages with switch statements (dart, java, etc).
170  if ([inputType isEqualToString:@"TextInputAction.unspecified"]) {
171  return UIReturnKeyDefault;
172  }
173 
174  if ([inputType isEqualToString:@"TextInputAction.done"]) {
175  return UIReturnKeyDone;
176  }
177 
178  if ([inputType isEqualToString:@"TextInputAction.go"]) {
179  return UIReturnKeyGo;
180  }
181 
182  if ([inputType isEqualToString:@"TextInputAction.send"]) {
183  return UIReturnKeySend;
184  }
185 
186  if ([inputType isEqualToString:@"TextInputAction.search"]) {
187  return UIReturnKeySearch;
188  }
189 
190  if ([inputType isEqualToString:@"TextInputAction.next"]) {
191  return UIReturnKeyNext;
192  }
193 
194  if ([inputType isEqualToString:@"TextInputAction.continueAction"]) {
195  return UIReturnKeyContinue;
196  }
197 
198  if ([inputType isEqualToString:@"TextInputAction.join"]) {
199  return UIReturnKeyJoin;
200  }
201 
202  if ([inputType isEqualToString:@"TextInputAction.route"]) {
203  return UIReturnKeyRoute;
204  }
205 
206  if ([inputType isEqualToString:@"TextInputAction.emergencyCall"]) {
207  return UIReturnKeyEmergencyCall;
208  }
209 
210  if ([inputType isEqualToString:@"TextInputAction.newline"]) {
211  return UIReturnKeyDefault;
212  }
213 
214  // Present default key if bad input type is given.
215  return UIReturnKeyDefault;
216 }
217 
218 static UITextContentType ToUITextContentType(NSArray<NSString*>* hints) {
219  if (!hints || hints.count == 0) {
220  // If no hints are specified, use the default content type nil.
221  return nil;
222  }
223 
224  NSString* hint = hints[0];
225  if ([hint isEqualToString:@"addressCityAndState"]) {
226  return UITextContentTypeAddressCityAndState;
227  }
228 
229  if ([hint isEqualToString:@"addressState"]) {
230  return UITextContentTypeAddressState;
231  }
232 
233  if ([hint isEqualToString:@"addressCity"]) {
234  return UITextContentTypeAddressCity;
235  }
236 
237  if ([hint isEqualToString:@"sublocality"]) {
238  return UITextContentTypeSublocality;
239  }
240 
241  if ([hint isEqualToString:@"streetAddressLine1"]) {
242  return UITextContentTypeStreetAddressLine1;
243  }
244 
245  if ([hint isEqualToString:@"streetAddressLine2"]) {
246  return UITextContentTypeStreetAddressLine2;
247  }
248 
249  if ([hint isEqualToString:@"countryName"]) {
250  return UITextContentTypeCountryName;
251  }
252 
253  if ([hint isEqualToString:@"fullStreetAddress"]) {
254  return UITextContentTypeFullStreetAddress;
255  }
256 
257  if ([hint isEqualToString:@"postalCode"]) {
258  return UITextContentTypePostalCode;
259  }
260 
261  if ([hint isEqualToString:@"location"]) {
262  return UITextContentTypeLocation;
263  }
264 
265  if ([hint isEqualToString:@"creditCardNumber"]) {
266  return UITextContentTypeCreditCardNumber;
267  }
268 
269  if ([hint isEqualToString:@"email"]) {
270  return UITextContentTypeEmailAddress;
271  }
272 
273  if ([hint isEqualToString:@"jobTitle"]) {
274  return UITextContentTypeJobTitle;
275  }
276 
277  if ([hint isEqualToString:@"givenName"]) {
278  return UITextContentTypeGivenName;
279  }
280 
281  if ([hint isEqualToString:@"middleName"]) {
282  return UITextContentTypeMiddleName;
283  }
284 
285  if ([hint isEqualToString:@"familyName"]) {
286  return UITextContentTypeFamilyName;
287  }
288 
289  if ([hint isEqualToString:@"name"]) {
290  return UITextContentTypeName;
291  }
292 
293  if ([hint isEqualToString:@"namePrefix"]) {
294  return UITextContentTypeNamePrefix;
295  }
296 
297  if ([hint isEqualToString:@"nameSuffix"]) {
298  return UITextContentTypeNameSuffix;
299  }
300 
301  if ([hint isEqualToString:@"nickname"]) {
302  return UITextContentTypeNickname;
303  }
304 
305  if ([hint isEqualToString:@"organizationName"]) {
306  return UITextContentTypeOrganizationName;
307  }
308 
309  if ([hint isEqualToString:@"telephoneNumber"]) {
310  return UITextContentTypeTelephoneNumber;
311  }
312 
313  if ([hint isEqualToString:@"password"]) {
314  return UITextContentTypePassword;
315  }
316 
317  if ([hint isEqualToString:@"oneTimeCode"]) {
318  return UITextContentTypeOneTimeCode;
319  }
320 
321  if ([hint isEqualToString:@"newPassword"]) {
322  return UITextContentTypeNewPassword;
323  }
324 
325  return hints[0];
326 }
327 
328 // Retrieves the autofillId from an input field's configuration. Returns
329 // nil if the field is nil and the input field is not a password field.
330 static NSString* AutofillIdFromDictionary(NSDictionary* dictionary) {
331  NSDictionary* autofill = dictionary[kAutofillProperties];
332  if (autofill) {
333  return autofill[kAutofillId];
334  }
335 
336  // When autofill is nil, the field may still need an autofill id
337  // if the field is for password.
338  return [dictionary[kSecureTextEntry] boolValue] ? @"password" : nil;
339 }
340 
341 // # Autofill Implementation Notes:
342 //
343 // Currently there're 2 types of autofills on iOS:
344 // - Regular autofill, including contact information and one-time-code,
345 // takes place in the form of predictive text in the quick type bar.
346 // This type of autofill does not save user input, and the keyboard
347 // currently only populates the focused field when a predictive text entry
348 // is selected by the user.
349 //
350 // - Password autofill, includes automatic strong password and regular
351 // password autofill. The former happens automatically when a
352 // "new password" field is detected and focused, and only that password
353 // field will be populated. The latter appears in the quick type bar when
354 // an eligible input field (which either has a UITextContentTypePassword
355 // contentType, or is a secure text entry) becomes the first responder, and may
356 // fill both the username and the password fields. iOS will attempt
357 // to save user input for both kinds of password fields. It's relatively
358 // tricky to deal with password autofill since it can autofill more than one
359 // field at a time and may employ heuristics based on what other text fields
360 // are in the same view controller.
361 //
362 // When a flutter text field is focused, and autofill is not explicitly disabled
363 // for it ("autofillable"), the framework collects its attributes and checks if
364 // it's in an AutofillGroup, and collects the attributes of other autofillable
365 // text fields in the same AutofillGroup if so. The attributes are sent to the
366 // text input plugin via a "TextInput.setClient" platform channel message. If
367 // autofill is disabled for a text field, its "autofill" field will be nil in
368 // the configuration json.
369 //
370 // The text input plugin then tries to determine which kind of autofill the text
371 // field needs. If the AutofillGroup the text field belongs to contains an
372 // autofillable text field that's password related, this text 's autofill type
373 // will be kFlutterAutofillTypePassword. If autofill is disabled for a text field,
374 // then its type will be kFlutterAutofillTypeNone. Otherwise the text field will
375 // have an autofill type of kFlutterAutofillTypeRegular.
376 //
377 // The text input plugin creates a new UIView for every kFlutterAutofillTypeNone
378 // text field. The UIView instance is never reused for other flutter text fields
379 // since the software keyboard often uses the identity of a UIView to distinguish
380 // different views and provides the same predictive text suggestions or restore
381 // the composing region if a UIView is reused for a different flutter text field.
382 //
383 // The text input plugin creates a new "autofill context" if the text field has
384 // the type of kFlutterAutofillTypePassword, to represent the AutofillGroup of
385 // the text field, and creates one FlutterTextInputView for every text field in
386 // the AutofillGroup.
387 //
388 // The text input plugin will try to reuse a UIView if a flutter text field's
389 // type is kFlutterAutofillTypeRegular, and has the same autofill id.
390 typedef NS_ENUM(NSInteger, FlutterAutofillType) {
391  // The field does not have autofillable content. Additionally if
392  // the field is currently in the autofill context, it will be
393  // removed from the context without triggering autofill save.
394  kFlutterAutofillTypeNone,
395  kFlutterAutofillTypeRegular,
396  kFlutterAutofillTypePassword,
397 };
398 
399 static BOOL IsFieldPasswordRelated(NSDictionary* configuration) {
400  // Autofill is explicitly disabled if the id isn't present.
401  if (!AutofillIdFromDictionary(configuration)) {
402  return NO;
403  }
404 
405  BOOL isSecureTextEntry = [configuration[kSecureTextEntry] boolValue];
406  if (isSecureTextEntry) {
407  return YES;
408  }
409 
410  NSDictionary* autofill = configuration[kAutofillProperties];
411  UITextContentType contentType = ToUITextContentType(autofill[kAutofillHints]);
412 
413  if ([contentType isEqualToString:UITextContentTypePassword] ||
414  [contentType isEqualToString:UITextContentTypeUsername]) {
415  return YES;
416  }
417 
418  if ([contentType isEqualToString:UITextContentTypeNewPassword]) {
419  return YES;
420  }
421 
422  return NO;
423 }
424 
425 static FlutterAutofillType AutofillTypeOf(NSDictionary* configuration) {
426  for (NSDictionary* field in configuration[kAssociatedAutofillFields]) {
427  if (IsFieldPasswordRelated(field)) {
428  return kFlutterAutofillTypePassword;
429  }
430  }
431 
432  if (IsFieldPasswordRelated(configuration)) {
433  return kFlutterAutofillTypePassword;
434  }
435 
436  NSDictionary* autofill = configuration[kAutofillProperties];
437  UITextContentType contentType = ToUITextContentType(autofill[kAutofillHints]);
438  return !autofill || [contentType isEqualToString:@""] ? kFlutterAutofillTypeNone
439  : kFlutterAutofillTypeRegular;
440 }
441 
442 static BOOL IsApproximatelyEqual(float x, float y, float delta) {
443  return fabsf(x - y) <= delta;
444 }
445 
446 // This is a helper function for floating cursor selection logic to determine which text
447 // position is closer to a point.
448 // Checks whether point should be considered closer to selectionRect compared to
449 // otherSelectionRect.
450 //
451 // If `useTrailingBoundaryOfSelectionRect` is not set, it uses the leading-center point
452 // on selectionRect and otherSelectionRect to compare.
453 // For left-to-right text, this means the left-center point, and for right-to-left text,
454 // this means the right-center point.
455 //
456 // If useTrailingBoundaryOfSelectionRect is set, the trailing-center point on selectionRect
457 // will be used instead of the leading-center point, while leading-center point is still used
458 // for otherSelectionRect.
459 //
460 // This uses special (empirically determined using a 1st gen iPad pro, 9.7" model running
461 // iOS 14.7.1) logic for determining the closer rect, rather than a simple distance calculation.
462 // - First, the rect with closer y distance wins.
463 // - Otherwise (same y distance):
464 // - If the point is above bottom of the rect, the rect boundary with closer x distance wins.
465 // - Otherwise (point is below bottom of the rect), the rect boundary with farthest x wins.
466 // This is because when the point is below the bottom line of text, we want to select the
467 // whole line of text, so we mark the farthest rect as closest.
468 static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point,
469  CGRect selectionRect,
470  BOOL selectionRectIsRTL,
471  BOOL useTrailingBoundaryOfSelectionRect,
472  CGRect otherSelectionRect,
473  BOOL otherSelectionRectIsRTL,
474  CGFloat verticalPrecision) {
475  // The point is inside the selectionRect's corresponding half-rect area.
476  if (CGRectContainsPoint(
477  CGRectMake(
478  selectionRect.origin.x + ((useTrailingBoundaryOfSelectionRect ^ selectionRectIsRTL)
479  ? 0.5 * selectionRect.size.width
480  : 0),
481  selectionRect.origin.y, 0.5 * selectionRect.size.width, selectionRect.size.height),
482  point)) {
483  return YES;
484  }
485  // pointForSelectionRect is either leading-center or trailing-center point of selectionRect.
486  CGPoint pointForSelectionRect = CGPointMake(
487  selectionRect.origin.x +
488  (selectionRectIsRTL ^ useTrailingBoundaryOfSelectionRect ? selectionRect.size.width : 0),
489  selectionRect.origin.y + selectionRect.size.height * 0.5);
490  float yDist = fabs(pointForSelectionRect.y - point.y);
491  float xDist = fabs(pointForSelectionRect.x - point.x);
492 
493  // pointForOtherSelectionRect is the leading-center point of otherSelectionRect.
494  CGPoint pointForOtherSelectionRect = CGPointMake(
495  otherSelectionRect.origin.x + (otherSelectionRectIsRTL ? otherSelectionRect.size.width : 0),
496  otherSelectionRect.origin.y + otherSelectionRect.size.height * 0.5);
497  float yDistOther = fabs(pointForOtherSelectionRect.y - point.y);
498  float xDistOther = fabs(pointForOtherSelectionRect.x - point.x);
499 
500  // This serves a similar purpose to IsApproximatelyEqual, allowing a little buffer before
501  // declaring something closer vertically to account for the small variations in size and position
502  // of SelectionRects, especially when dealing with emoji.
503  BOOL isCloserVertically = yDist < yDistOther - verticalPrecision;
504  BOOL isEqualVertically = IsApproximatelyEqual(yDist, yDistOther, verticalPrecision);
505  BOOL isAboveBottomOfLine = point.y <= selectionRect.origin.y + selectionRect.size.height;
506  BOOL isCloserHorizontally = xDist < xDistOther;
507  BOOL isBelowBottomOfLine = point.y > selectionRect.origin.y + selectionRect.size.height;
508  // Is "farther away", or is closer to the end of the text line.
509  BOOL isFarther;
510  if (selectionRectIsRTL) {
511  isFarther = selectionRect.origin.x < otherSelectionRect.origin.x;
512  } else {
513  isFarther = selectionRect.origin.x +
514  (useTrailingBoundaryOfSelectionRect ? selectionRect.size.width : 0) >
515  otherSelectionRect.origin.x;
516  }
517  return (isCloserVertically ||
518  (isEqualVertically &&
519  ((isAboveBottomOfLine && isCloserHorizontally) || (isBelowBottomOfLine && isFarther))));
520 }
521 
522 #pragma mark - FlutterTextPosition
523 
524 @implementation FlutterTextPosition
525 
526 + (instancetype)positionWithIndex:(NSUInteger)index {
527  return [[FlutterTextPosition alloc] initWithIndex:index affinity:UITextStorageDirectionForward];
528 }
529 
530 + (instancetype)positionWithIndex:(NSUInteger)index affinity:(UITextStorageDirection)affinity {
531  return [[FlutterTextPosition alloc] initWithIndex:index affinity:affinity];
532 }
533 
534 - (instancetype)initWithIndex:(NSUInteger)index affinity:(UITextStorageDirection)affinity {
535  self = [super init];
536  if (self) {
537  _index = index;
538  _affinity = affinity;
539  }
540  return self;
541 }
542 
543 @end
544 
545 #pragma mark - FlutterTextRange
546 
547 @implementation FlutterTextRange
548 
549 + (instancetype)rangeWithNSRange:(NSRange)range {
550  return [[FlutterTextRange alloc] initWithNSRange:range];
551 }
552 
553 - (instancetype)initWithNSRange:(NSRange)range {
554  self = [super init];
555  if (self) {
556  _range = range;
557  }
558  return self;
559 }
560 
561 - (UITextPosition*)start {
562  return [FlutterTextPosition positionWithIndex:self.range.location
563  affinity:UITextStorageDirectionForward];
564 }
565 
566 - (UITextPosition*)end {
567  return [FlutterTextPosition positionWithIndex:self.range.location + self.range.length
568  affinity:UITextStorageDirectionBackward];
569 }
570 
571 - (BOOL)isEmpty {
572  return self.range.length == 0;
573 }
574 
575 - (id)copyWithZone:(NSZone*)zone {
576  return [[FlutterTextRange allocWithZone:zone] initWithNSRange:self.range];
577 }
578 
579 - (BOOL)isEqualTo:(FlutterTextRange*)other {
580  return NSEqualRanges(self.range, other.range);
581 }
582 @end
583 
584 #pragma mark - FlutterTokenizer
585 
586 @interface FlutterTokenizer ()
587 
588 @property(nonatomic, weak) FlutterTextInputView* textInputView;
589 
590 @end
591 
592 @implementation FlutterTokenizer
593 
594 - (instancetype)initWithTextInput:(UIResponder<UITextInput>*)textInput {
595  NSAssert([textInput isKindOfClass:[FlutterTextInputView class]],
596  @"The FlutterTokenizer can only be used in a FlutterTextInputView");
597  self = [super initWithTextInput:textInput];
598  if (self) {
599  _textInputView = (FlutterTextInputView*)textInput;
600  }
601  return self;
602 }
603 
604 - (UITextRange*)rangeEnclosingPosition:(UITextPosition*)position
605  withGranularity:(UITextGranularity)granularity
606  inDirection:(UITextDirection)direction {
607  UITextRange* result;
608  switch (granularity) {
609  case UITextGranularityLine:
610  // The default UITextInputStringTokenizer does not handle line granularity
611  // correctly. We need to implement our own line tokenizer.
612  result = [self lineEnclosingPosition:position inDirection:direction];
613  break;
614  case UITextGranularityCharacter:
615  case UITextGranularityWord:
616  case UITextGranularitySentence:
617  case UITextGranularityParagraph:
618  case UITextGranularityDocument:
619  // The UITextInputStringTokenizer can handle all these cases correctly.
620  result = [super rangeEnclosingPosition:position
621  withGranularity:granularity
622  inDirection:direction];
623  break;
624  }
625  return result;
626 }
627 
628 - (UITextRange*)lineEnclosingPosition:(UITextPosition*)position
629  inDirection:(UITextDirection)direction {
630  // TODO(hellohuanlin): remove iOS 17 check. The same logic should apply to older iOS version.
631  if (@available(iOS 17.0, *)) {
632  // According to the API doc if the text position is at a text-unit boundary, it is considered
633  // enclosed only if the next position in the given direction is entirely enclosed. Link:
634  // https://developer.apple.com/documentation/uikit/uitextinputtokenizer/1614464-rangeenclosingposition?language=objc
635  FlutterTextPosition* flutterPosition = (FlutterTextPosition*)position;
636  if (flutterPosition.index > _textInputView.text.length ||
637  (flutterPosition.index == _textInputView.text.length &&
638  direction == UITextStorageDirectionForward)) {
639  return nil;
640  }
641  }
642 
643  // Gets the first line break position after the input position.
644  NSString* textAfter = [_textInputView
645  textInRange:[_textInputView textRangeFromPosition:position
646  toPosition:[_textInputView endOfDocument]]];
647  NSArray<NSString*>* linesAfter = [textAfter componentsSeparatedByString:@"\n"];
648  NSInteger offSetToLineBreak = [linesAfter firstObject].length;
649  UITextPosition* lineBreakAfter = [_textInputView positionFromPosition:position
650  offset:offSetToLineBreak];
651  // Gets the first line break position before the input position.
652  NSString* textBefore = [_textInputView
653  textInRange:[_textInputView textRangeFromPosition:[_textInputView beginningOfDocument]
654  toPosition:position]];
655  NSArray<NSString*>* linesBefore = [textBefore componentsSeparatedByString:@"\n"];
656  NSInteger offSetFromLineBreak = [linesBefore lastObject].length;
657  UITextPosition* lineBreakBefore = [_textInputView positionFromPosition:position
658  offset:-offSetFromLineBreak];
659 
660  return [_textInputView textRangeFromPosition:lineBreakBefore toPosition:lineBreakAfter];
661 }
662 
663 @end
664 
665 #pragma mark - FlutterTextSelectionRect
666 
667 @implementation FlutterTextSelectionRect
668 
669 // Synthesize properties declared readonly in UITextSelectionRect.
670 @synthesize rect = _rect;
671 @synthesize writingDirection = _writingDirection;
672 @synthesize containsStart = _containsStart;
673 @synthesize containsEnd = _containsEnd;
674 @synthesize isVertical = _isVertical;
675 
676 + (instancetype)selectionRectWithRectAndInfo:(CGRect)rect
677  position:(NSUInteger)position
678  writingDirection:(NSWritingDirection)writingDirection
679  containsStart:(BOOL)containsStart
680  containsEnd:(BOOL)containsEnd
681  isVertical:(BOOL)isVertical {
682  return [[FlutterTextSelectionRect alloc] initWithRectAndInfo:rect
683  position:position
684  writingDirection:writingDirection
685  containsStart:containsStart
686  containsEnd:containsEnd
687  isVertical:isVertical];
688 }
689 
690 + (instancetype)selectionRectWithRect:(CGRect)rect position:(NSUInteger)position {
691  return [[FlutterTextSelectionRect alloc] initWithRectAndInfo:rect
692  position:position
693  writingDirection:NSWritingDirectionNatural
694  containsStart:NO
695  containsEnd:NO
696  isVertical:NO];
697 }
698 
699 + (instancetype)selectionRectWithRect:(CGRect)rect
700  position:(NSUInteger)position
701  writingDirection:(NSWritingDirection)writingDirection {
702  return [[FlutterTextSelectionRect alloc] initWithRectAndInfo:rect
703  position:position
704  writingDirection:writingDirection
705  containsStart:NO
706  containsEnd:NO
707  isVertical:NO];
708 }
709 
710 - (instancetype)initWithRectAndInfo:(CGRect)rect
711  position:(NSUInteger)position
712  writingDirection:(NSWritingDirection)writingDirection
713  containsStart:(BOOL)containsStart
714  containsEnd:(BOOL)containsEnd
715  isVertical:(BOOL)isVertical {
716  self = [super init];
717  if (self) {
718  self.rect = rect;
719  self.position = position;
720  self.writingDirection = writingDirection;
721  self.containsStart = containsStart;
722  self.containsEnd = containsEnd;
723  self.isVertical = isVertical;
724  }
725  return self;
726 }
727 
728 - (BOOL)isRTL {
729  return _writingDirection == NSWritingDirectionRightToLeft;
730 }
731 
732 @end
733 
734 #pragma mark - FlutterTextPlaceholder
735 
736 @implementation FlutterTextPlaceholder
737 
738 - (NSArray<UITextSelectionRect*>*)rects {
739  // Returning anything other than an empty array here seems to cause PencilKit to enter an
740  // infinite loop of allocating placeholders until the app crashes
741  return @[];
742 }
743 
744 @end
745 
746 // A FlutterTextInputView that masquerades as a UITextField, and forwards
747 // selectors it can't respond to a shared UITextField instance.
748 //
749 // Relevant API docs claim that password autofill supports any custom view
750 // that adopts the UITextInput protocol, automatic strong password seems to
751 // currently only support UITextFields, and password saving only supports
752 // UITextFields and UITextViews, as of iOS 13.5.
754 @property(nonatomic, retain, readonly) UITextField* textField;
755 @end
756 
757 @implementation FlutterSecureTextInputView {
758  UITextField* _textField;
759 }
760 
761 - (UITextField*)textField {
762  if (!_textField) {
763  _textField = [[UITextField alloc] init];
764  }
765  return _textField;
766 }
767 
768 - (BOOL)isKindOfClass:(Class)aClass {
769  return [super isKindOfClass:aClass] || (aClass == [UITextField class]);
770 }
771 
772 - (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector {
773  NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
774  if (!signature) {
775  signature = [self.textField methodSignatureForSelector:aSelector];
776  }
777  return signature;
778 }
779 
780 - (void)forwardInvocation:(NSInvocation*)anInvocation {
781  [anInvocation invokeWithTarget:self.textField];
782 }
783 
784 @end
785 
787 @property(nonatomic, readonly, weak) id<FlutterTextInputDelegate> textInputDelegate;
788 @property(nonatomic, readonly) UIView* hostView;
789 @end
790 
791 @interface FlutterTextInputView ()
792 @property(nonatomic, readonly, weak) FlutterTextInputPlugin* textInputPlugin;
793 @property(nonatomic, copy) NSString* autofillId;
794 @property(nonatomic, readonly) CATransform3D editableTransform;
795 @property(nonatomic, assign) CGRect markedRect;
796 // Disables the cursor from dismissing when firstResponder is resigned
797 @property(nonatomic, assign) BOOL preventCursorDismissWhenResignFirstResponder;
798 @property(nonatomic) BOOL isVisibleToAutofill;
799 @property(nonatomic, assign) BOOL accessibilityEnabled;
800 @property(nonatomic, assign) int textInputClient;
801 // The composed character that is temporarily removed by the keyboard API.
802 // This is cleared at the start of each keyboard interaction. (Enter a character, delete a character
803 // etc)
804 @property(nonatomic, copy) NSString* temporarilyDeletedComposedCharacter;
805 @property(nonatomic, assign) CGRect editMenuTargetRect;
806 @property(nonatomic, strong) NSArray<NSDictionary*>* editMenuItems;
807 
808 - (void)setEditableTransform:(NSArray*)matrix;
809 @end
810 
811 @implementation FlutterTextInputView {
812  int _textInputClient;
813  const char* _selectionAffinity;
815  UIInputViewController* _inputViewController;
817  FlutterScribbleInteractionStatus _scribbleInteractionStatus;
819  // Whether to show the system keyboard when this view
820  // becomes the first responder. Typically set to false
821  // when the app shows its own in-flutter keyboard.
826  UITextInteraction* _textInteraction API_AVAILABLE(ios(13.0));
827 }
828 
829 @synthesize tokenizer = _tokenizer;
830 
831 - (instancetype)initWithOwner:(FlutterTextInputPlugin*)textInputPlugin {
832  self = [super initWithFrame:CGRectZero];
833  if (self) {
834  _textInputPlugin = textInputPlugin;
835  _textInputClient = 0;
837  _preventCursorDismissWhenResignFirstResponder = NO;
838 
839  // UITextInput
840  _text = [[NSMutableString alloc] init];
841  _selectedTextRange = [[FlutterTextRange alloc] initWithNSRange:NSMakeRange(0, 0)];
842  _markedRect = kInvalidFirstRect;
844  _scribbleInteractionStatus = FlutterScribbleInteractionStatusNone;
845  _pendingDeltas = [[NSMutableArray alloc] init];
846  // Initialize with the zero matrix which is not
847  // an affine transform.
848  _editableTransform = CATransform3D();
849 
850  // UITextInputTraits
851  _autocapitalizationType = UITextAutocapitalizationTypeSentences;
852  _autocorrectionType = UITextAutocorrectionTypeDefault;
853  _spellCheckingType = UITextSpellCheckingTypeDefault;
854  _enablesReturnKeyAutomatically = NO;
855  _keyboardAppearance = UIKeyboardAppearanceDefault;
856  _keyboardType = UIKeyboardTypeDefault;
857  _returnKeyType = UIReturnKeyDone;
858  _secureTextEntry = NO;
859  _enableDeltaModel = NO;
861  _accessibilityEnabled = NO;
862  _smartQuotesType = UITextSmartQuotesTypeYes;
863  _smartDashesType = UITextSmartDashesTypeYes;
864  _selectionRects = [[NSArray alloc] init];
865 
866  if (@available(iOS 14.0, *)) {
867  UIScribbleInteraction* interaction = [[UIScribbleInteraction alloc] initWithDelegate:self];
868  [self addInteraction:interaction];
869  }
870  }
871 
872  if (@available(iOS 16.0, *)) {
873  _editMenuInteraction = [[UIEditMenuInteraction alloc] initWithDelegate:self];
874  [self addInteraction:_editMenuInteraction];
875  }
876 
877  return self;
878 }
879 
880 - (void)handleSearchWebAction {
881  [self.textInputDelegate flutterTextInputView:self
882  searchWebWithSelectedText:[self textInRange:_selectedTextRange]];
883 }
884 
885 - (void)handleLookUpAction {
886  [self.textInputDelegate flutterTextInputView:self
887  lookUpSelectedText:[self textInRange:_selectedTextRange]];
888 }
889 
890 - (void)handleShareAction {
891  [self.textInputDelegate flutterTextInputView:self
892  shareSelectedText:[self textInRange:_selectedTextRange]];
893 }
894 
895 // DFS algorithm to search a UICommand from the menu tree.
896 - (UICommand*)searchCommandWithSelector:(SEL)selector
897  element:(UIMenuElement*)element API_AVAILABLE(ios(16.0)) {
898  if ([element isKindOfClass:UICommand.class]) {
899  UICommand* command = (UICommand*)element;
900  return command.action == selector ? command : nil;
901  } else if ([element isKindOfClass:UIMenu.class]) {
902  NSArray<UIMenuElement*>* children = ((UIMenu*)element).children;
903  for (UIMenuElement* child in children) {
904  UICommand* result = [self searchCommandWithSelector:selector element:child];
905  if (result) {
906  return result;
907  }
908  }
909  return nil;
910  } else {
911  return nil;
912  }
913 }
914 
915 - (void)addBasicEditingCommandToItems:(NSMutableArray*)items
916  type:(NSString*)type
917  selector:(SEL)selector
918  suggestedMenu:(UIMenu*)suggestedMenu {
919  UICommand* command = [self searchCommandWithSelector:selector element:suggestedMenu];
920  if (command) {
921  [items addObject:command];
922  } else {
923  FML_LOG(ERROR) << "Cannot find context menu item of type \"" << type.UTF8String << "\".";
924  }
925 }
926 
927 - (void)addAdditionalBasicCommandToItems:(NSMutableArray*)items
928  type:(NSString*)type
929  selector:(SEL)selector
930  encodedItem:(NSDictionary<NSString*, id>*)encodedItem {
931  NSString* title = encodedItem[@"title"];
932  if (title) {
933  UICommand* command = [UICommand commandWithTitle:title
934  image:nil
935  action:selector
936  propertyList:nil];
937  [items addObject:command];
938  } else {
939  FML_LOG(ERROR) << "Missing title for context menu item of type \"" << type.UTF8String << "\".";
940  }
941 }
942 
943 - (UIMenu*)editMenuInteraction:(UIEditMenuInteraction*)interaction
944  menuForConfiguration:(UIEditMenuConfiguration*)configuration
945  suggestedActions:(NSArray<UIMenuElement*>*)suggestedActions API_AVAILABLE(ios(16.0)) {
946  UIMenu* suggestedMenu = [UIMenu menuWithChildren:suggestedActions];
947  if (!_editMenuItems) {
948  return suggestedMenu;
949  }
950 
951  NSMutableArray* items = [NSMutableArray array];
952  for (NSDictionary<NSString*, id>* encodedItem in _editMenuItems) {
953  NSString* type = encodedItem[@"type"];
954  if ([type isEqualToString:@"copy"]) {
955  [self addBasicEditingCommandToItems:items
956  type:type
957  selector:@selector(copy:)
958  suggestedMenu:suggestedMenu];
959  } else if ([type isEqualToString:@"paste"]) {
960  [self addBasicEditingCommandToItems:items
961  type:type
962  selector:@selector(paste:)
963  suggestedMenu:suggestedMenu];
964  } else if ([type isEqualToString:@"cut"]) {
965  [self addBasicEditingCommandToItems:items
966  type:type
967  selector:@selector(cut:)
968  suggestedMenu:suggestedMenu];
969  } else if ([type isEqualToString:@"delete"]) {
970  [self addBasicEditingCommandToItems:items
971  type:type
972  selector:@selector(delete:)
973  suggestedMenu:suggestedMenu];
974  } else if ([type isEqualToString:@"selectAll"]) {
975  [self addBasicEditingCommandToItems:items
976  type:type
977  selector:@selector(selectAll:)
978  suggestedMenu:suggestedMenu];
979  } else if ([type isEqualToString:@"searchWeb"]) {
980  [self addAdditionalBasicCommandToItems:items
981  type:type
982  selector:@selector(handleSearchWebAction)
983  encodedItem:encodedItem];
984  } else if ([type isEqualToString:@"share"]) {
985  [self addAdditionalBasicCommandToItems:items
986  type:type
987  selector:@selector(handleShareAction)
988  encodedItem:encodedItem];
989  } else if ([type isEqualToString:@"lookUp"]) {
990  [self addAdditionalBasicCommandToItems:items
991  type:type
992  selector:@selector(handleLookUpAction)
993  encodedItem:encodedItem];
994  }
995  }
996  return [UIMenu menuWithChildren:items];
997 }
998 
999 - (void)editMenuInteraction:(UIEditMenuInteraction*)interaction
1000  willDismissMenuForConfiguration:(UIEditMenuConfiguration*)configuration
1001  animator:(id<UIEditMenuInteractionAnimating>)animator
1002  API_AVAILABLE(ios(16.0)) {
1003  [self.textInputDelegate flutterTextInputView:self
1004  willDismissEditMenuWithTextInputClient:_textInputClient];
1005 }
1006 
1007 - (CGRect)editMenuInteraction:(UIEditMenuInteraction*)interaction
1008  targetRectForConfiguration:(UIEditMenuConfiguration*)configuration API_AVAILABLE(ios(16.0)) {
1009  return _editMenuTargetRect;
1010 }
1011 
1012 - (void)showEditMenuWithTargetRect:(CGRect)targetRect
1013  items:(NSArray<NSDictionary*>*)items API_AVAILABLE(ios(16.0)) {
1014  _editMenuTargetRect = targetRect;
1015  _editMenuItems = items;
1016  UIEditMenuConfiguration* config =
1017  [UIEditMenuConfiguration configurationWithIdentifier:nil sourcePoint:CGPointZero];
1018  [self.editMenuInteraction presentEditMenuWithConfiguration:config];
1019 }
1020 
1021 - (void)hideEditMenu API_AVAILABLE(ios(16.0)) {
1022  [self.editMenuInteraction dismissMenu];
1023 }
1024 
1025 - (void)configureWithDictionary:(NSDictionary*)configuration {
1026  NSDictionary* inputType = configuration[kKeyboardType];
1027  NSString* keyboardAppearance = configuration[kKeyboardAppearance];
1028  NSDictionary* autofill = configuration[kAutofillProperties];
1029 
1030  self.secureTextEntry = [configuration[kSecureTextEntry] boolValue];
1031  self.enableDeltaModel = [configuration[kEnableDeltaModel] boolValue];
1032 
1034  self.keyboardType = ToUIKeyboardType(inputType);
1035  self.returnKeyType = ToUIReturnKeyType(configuration[kInputAction]);
1036  self.autocapitalizationType = ToUITextAutoCapitalizationType(configuration);
1037  _enableInteractiveSelection = [configuration[kEnableInteractiveSelection] boolValue];
1038  NSString* smartDashesType = configuration[kSmartDashesType];
1039  // This index comes from the SmartDashesType enum in the framework.
1040  bool smartDashesIsDisabled = smartDashesType && [smartDashesType isEqualToString:@"0"];
1041  self.smartDashesType = smartDashesIsDisabled ? UITextSmartDashesTypeNo : UITextSmartDashesTypeYes;
1042  NSString* smartQuotesType = configuration[kSmartQuotesType];
1043  // This index comes from the SmartQuotesType enum in the framework.
1044  bool smartQuotesIsDisabled = smartQuotesType && [smartQuotesType isEqualToString:@"0"];
1045  self.smartQuotesType = smartQuotesIsDisabled ? UITextSmartQuotesTypeNo : UITextSmartQuotesTypeYes;
1046  if ([keyboardAppearance isEqualToString:@"Brightness.dark"]) {
1047  self.keyboardAppearance = UIKeyboardAppearanceDark;
1048  } else if ([keyboardAppearance isEqualToString:@"Brightness.light"]) {
1049  self.keyboardAppearance = UIKeyboardAppearanceLight;
1050  } else {
1051  self.keyboardAppearance = UIKeyboardAppearanceDefault;
1052  }
1053  NSString* autocorrect = configuration[kAutocorrectionType];
1054  bool autocorrectIsDisabled = autocorrect && ![autocorrect boolValue];
1055  self.autocorrectionType =
1056  autocorrectIsDisabled ? UITextAutocorrectionTypeNo : UITextAutocorrectionTypeDefault;
1057  self.spellCheckingType =
1058  autocorrectIsDisabled ? UITextSpellCheckingTypeNo : UITextSpellCheckingTypeDefault;
1059  self.autofillId = AutofillIdFromDictionary(configuration);
1060  if (autofill == nil) {
1061  self.textContentType = @"";
1062  } else {
1063  self.textContentType = ToUITextContentType(autofill[kAutofillHints]);
1064  [self setTextInputState:autofill[kAutofillEditingValue]];
1065  NSAssert(_autofillId, @"The autofill configuration must contain an autofill id");
1066  }
1067  // The input field needs to be visible for the system autofill
1068  // to find it.
1069  self.isVisibleToAutofill = autofill || _secureTextEntry;
1070 }
1071 
1072 - (UITextContentType)textContentType {
1073  return _textContentType;
1074 }
1075 
1076 // Prevent UIKit from showing selection handles or highlights. This is needed
1077 // because Scribble interactions require the view to have it's actual frame on
1078 // the screen. They're not needed on iOS 17 with the new
1079 // UITextSelectionDisplayInteraction API.
1080 //
1081 // These are undocumented methods. On iOS 17, the insertion point color is also
1082 // used as the highlighted background of the selected IME candidate:
1083 // https://github.com/flutter/flutter/issues/132548
1084 // So the respondsToSelector method is overridden to return NO for this method
1085 // on iOS 17+.
1086 - (UIColor*)insertionPointColor {
1087  return [UIColor clearColor];
1088 }
1089 
1090 - (UIColor*)selectionBarColor {
1091  return [UIColor clearColor];
1092 }
1093 
1094 - (UIColor*)selectionHighlightColor {
1095  return [UIColor clearColor];
1096 }
1097 
1098 - (UIInputViewController*)inputViewController {
1100  return nil;
1101  }
1102 
1103  if (!_inputViewController) {
1104  _inputViewController = [[UIInputViewController alloc] init];
1105  }
1106  return _inputViewController;
1107 }
1108 
1109 - (id<FlutterTextInputDelegate>)textInputDelegate {
1110  return _textInputPlugin.textInputDelegate;
1111 }
1112 
1113 - (BOOL)respondsToSelector:(SEL)selector {
1114  if (@available(iOS 17.0, *)) {
1115  // See the comment on this method.
1116  if (selector == @selector(insertionPointColor)) {
1117  return NO;
1118  }
1119  }
1120  return [super respondsToSelector:selector];
1121 }
1122 
1123 - (void)setTextInputClient:(int)client {
1124  _textInputClient = client;
1125  _hasPlaceholder = NO;
1126 }
1127 
1128 - (UITextInteraction*)textInteraction API_AVAILABLE(ios(13.0)) {
1129  if (!_textInteraction) {
1130  _textInteraction = [UITextInteraction textInteractionForMode:UITextInteractionModeEditable];
1131  _textInteraction.textInput = self;
1132  }
1133  return _textInteraction;
1134 }
1135 
1136 - (void)setTextInputState:(NSDictionary*)state {
1137  if (@available(iOS 13.0, *)) {
1138  // [UITextInteraction willMoveToView:] sometimes sets the textInput's inputDelegate
1139  // to nil. This is likely a bug in UIKit. In order to inform the keyboard of text
1140  // and selection changes when that happens, add a dummy UITextInteraction to this
1141  // view so it sets a valid inputDelegate that we can call textWillChange et al. on.
1142  // See https://github.com/flutter/engine/pull/32881.
1143  if (!self.inputDelegate && self.isFirstResponder) {
1144  [self addInteraction:self.textInteraction];
1145  }
1146  }
1147 
1148  NSString* newText = state[@"text"];
1149  BOOL textChanged = ![self.text isEqualToString:newText];
1150  if (textChanged) {
1151  [self.inputDelegate textWillChange:self];
1152  [self.text setString:newText];
1153  }
1154  NSInteger composingBase = [state[@"composingBase"] intValue];
1155  NSInteger composingExtent = [state[@"composingExtent"] intValue];
1156  NSRange composingRange = [self clampSelection:NSMakeRange(MIN(composingBase, composingExtent),
1157  ABS(composingBase - composingExtent))
1158  forText:self.text];
1159 
1160  self.markedTextRange =
1161  composingRange.length > 0 ? [FlutterTextRange rangeWithNSRange:composingRange] : nil;
1162 
1163  NSRange selectedRange = [self clampSelectionFromBase:[state[@"selectionBase"] intValue]
1164  extent:[state[@"selectionExtent"] intValue]
1165  forText:self.text];
1166 
1167  NSRange oldSelectedRange = [(FlutterTextRange*)self.selectedTextRange range];
1168  if (!NSEqualRanges(selectedRange, oldSelectedRange)) {
1169  [self.inputDelegate selectionWillChange:self];
1170 
1171  [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]];
1172 
1174  if ([state[@"selectionAffinity"] isEqualToString:@(kTextAffinityUpstream)]) {
1176  }
1177  [self.inputDelegate selectionDidChange:self];
1178  }
1179 
1180  if (textChanged) {
1181  [self.inputDelegate textDidChange:self];
1182  }
1183 
1184  if (@available(iOS 13.0, *)) {
1185  if (_textInteraction) {
1186  [self removeInteraction:_textInteraction];
1187  }
1188  }
1189 }
1190 
1191 // Forward touches to the viewResponder to allow tapping inside the UITextField as normal.
1192 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
1193  _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1194  [self resetScribbleInteractionStatusIfEnding];
1195  [self.viewResponder touchesBegan:touches withEvent:event];
1196 }
1197 
1198 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
1199  [self.viewResponder touchesMoved:touches withEvent:event];
1200 }
1201 
1202 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
1203  [self.viewResponder touchesEnded:touches withEvent:event];
1204 }
1205 
1206 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1207  [self.viewResponder touchesCancelled:touches withEvent:event];
1208 }
1209 
1210 - (void)touchesEstimatedPropertiesUpdated:(NSSet*)touches {
1211  [self.viewResponder touchesEstimatedPropertiesUpdated:touches];
1212 }
1213 
1214 // Extracts the selection information from the editing state dictionary.
1215 //
1216 // The state may contain an invalid selection, such as when no selection was
1217 // explicitly set in the framework. This is handled here by setting the
1218 // selection to (0,0). In contrast, Android handles this situation by
1219 // clearing the selection, but the result in both cases is that the cursor
1220 // is placed at the beginning of the field.
1221 - (NSRange)clampSelectionFromBase:(int)selectionBase
1222  extent:(int)selectionExtent
1223  forText:(NSString*)text {
1224  int loc = MIN(selectionBase, selectionExtent);
1225  int len = ABS(selectionExtent - selectionBase);
1226  return loc < 0 ? NSMakeRange(0, 0)
1227  : [self clampSelection:NSMakeRange(loc, len) forText:self.text];
1228 }
1229 
1230 - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text {
1231  NSUInteger start = MIN(MAX(range.location, 0), text.length);
1232  NSUInteger length = MIN(range.length, text.length - start);
1233  return NSMakeRange(start, length);
1234 }
1235 
1236 - (BOOL)isVisibleToAutofill {
1237  return self.frame.size.width > 0 && self.frame.size.height > 0;
1238 }
1239 
1240 // An input view is generally ignored by password autofill attempts, if it's
1241 // not the first responder and is zero-sized. For input fields that are in the
1242 // autofill context but do not belong to the current autofill group, setting
1243 // their frames to CGRectZero prevents ios autofill from taking them into
1244 // account.
1245 - (void)setIsVisibleToAutofill:(BOOL)isVisibleToAutofill {
1246  // This probably needs to change (think it is getting overwritten by the updateSizeAndTransform
1247  // stuff for now).
1248  self.frame = isVisibleToAutofill ? CGRectMake(0, 0, 1, 1) : CGRectZero;
1249 }
1250 
1251 #pragma mark UIScribbleInteractionDelegate
1252 
1253 // Checks whether Scribble features are possibly available – meaning this is an iPad running iOS
1254 // 14 or higher.
1255 - (BOOL)isScribbleAvailable {
1256  if (@available(iOS 14.0, *)) {
1257  if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
1258  return YES;
1259  }
1260  }
1261  return NO;
1262 }
1263 
1264 - (void)scribbleInteractionWillBeginWriting:(UIScribbleInteraction*)interaction
1265  API_AVAILABLE(ios(14.0)) {
1266  _scribbleInteractionStatus = FlutterScribbleInteractionStatusStarted;
1267  [self.textInputDelegate flutterTextInputViewScribbleInteractionBegan:self];
1268 }
1269 
1270 - (void)scribbleInteractionDidFinishWriting:(UIScribbleInteraction*)interaction
1271  API_AVAILABLE(ios(14.0)) {
1272  _scribbleInteractionStatus = FlutterScribbleInteractionStatusEnding;
1273  [self.textInputDelegate flutterTextInputViewScribbleInteractionFinished:self];
1274 }
1275 
1276 - (BOOL)scribbleInteraction:(UIScribbleInteraction*)interaction
1277  shouldBeginAtLocation:(CGPoint)location API_AVAILABLE(ios(14.0)) {
1278  return YES;
1279 }
1280 
1281 - (BOOL)scribbleInteractionShouldDelayFocus:(UIScribbleInteraction*)interaction
1282  API_AVAILABLE(ios(14.0)) {
1283  return NO;
1284 }
1285 
1286 #pragma mark - UIResponder Overrides
1287 
1288 - (BOOL)canBecomeFirstResponder {
1289  // Only the currently focused input field can
1290  // become the first responder. This prevents iOS
1291  // from changing focus by itself (the framework
1292  // focus will be out of sync if that happens).
1293  return _textInputClient != 0;
1294 }
1295 
1296 - (BOOL)resignFirstResponder {
1297  BOOL success = [super resignFirstResponder];
1298  if (success) {
1299  if (!_preventCursorDismissWhenResignFirstResponder) {
1300  [self.textInputDelegate flutterTextInputView:self
1301  didResignFirstResponderWithTextInputClient:_textInputClient];
1302  }
1303  }
1304  return success;
1305 }
1306 
1307 - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
1308  if (action == @selector(paste:)) {
1309  // Forbid pasting images, memojis, or other non-string content.
1310  return [UIPasteboard generalPasteboard].hasStrings;
1311  } else if (action == @selector(copy:) || action == @selector(cut:) ||
1312  action == @selector(delete:)) {
1313  return [self textInRange:_selectedTextRange].length > 0;
1314  } else if (action == @selector(selectAll:)) {
1315  return self.hasText;
1316  }
1317  return [super canPerformAction:action withSender:sender];
1318 }
1319 
1320 #pragma mark - UIResponderStandardEditActions Overrides
1321 
1322 - (void)cut:(id)sender {
1323  [UIPasteboard generalPasteboard].string = [self textInRange:_selectedTextRange];
1324  [self replaceRange:_selectedTextRange withText:@""];
1325 }
1326 
1327 - (void)copy:(id)sender {
1328  [UIPasteboard generalPasteboard].string = [self textInRange:_selectedTextRange];
1329 }
1330 
1331 - (void)paste:(id)sender {
1332  NSString* pasteboardString = [UIPasteboard generalPasteboard].string;
1333  if (pasteboardString != nil) {
1334  [self insertText:pasteboardString];
1335  }
1336 }
1337 
1338 - (void)delete:(id)sender {
1339  [self replaceRange:_selectedTextRange withText:@""];
1340 }
1341 
1342 - (void)selectAll:(id)sender {
1343  [self setSelectedTextRange:[self textRangeFromPosition:[self beginningOfDocument]
1344  toPosition:[self endOfDocument]]];
1345 }
1346 
1347 #pragma mark - UITextInput Overrides
1348 
1349 - (id<UITextInputTokenizer>)tokenizer {
1350  if (_tokenizer == nil) {
1351  _tokenizer = [[FlutterTokenizer alloc] initWithTextInput:self];
1352  }
1353  return _tokenizer;
1354 }
1355 
1356 - (UITextRange*)selectedTextRange {
1357  return [_selectedTextRange copy];
1358 }
1359 
1360 // Change the range of selected text, without notifying the framework.
1361 - (void)setSelectedTextRangeLocal:(UITextRange*)selectedTextRange {
1363  if (self.hasText) {
1364  FlutterTextRange* flutterTextRange = (FlutterTextRange*)selectedTextRange;
1366  rangeWithNSRange:fml::RangeForCharactersInRange(self.text, flutterTextRange.range)] copy];
1367  } else {
1368  _selectedTextRange = [selectedTextRange copy];
1369  }
1370  }
1371 }
1372 
1373 - (void)setSelectedTextRange:(UITextRange*)selectedTextRange {
1375  return;
1376  }
1377 
1378  [self setSelectedTextRangeLocal:selectedTextRange];
1379 
1380  if (_enableDeltaModel) {
1381  [self updateEditingStateWithDelta:flutter::TextEditingDelta([self.text UTF8String])];
1382  } else {
1383  [self updateEditingState];
1384  }
1385 
1386  if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone ||
1387  _scribbleFocusStatus == FlutterScribbleFocusStatusFocused) {
1388  NSAssert([selectedTextRange isKindOfClass:[FlutterTextRange class]],
1389  @"Expected a FlutterTextRange for range (got %@).", [selectedTextRange class]);
1390  FlutterTextRange* flutterTextRange = (FlutterTextRange*)selectedTextRange;
1391  if (flutterTextRange.range.length > 0) {
1392  [self.textInputDelegate flutterTextInputView:self showToolbar:_textInputClient];
1393  }
1394  }
1395 
1396  [self resetScribbleInteractionStatusIfEnding];
1397 }
1398 
1399 - (id)insertDictationResultPlaceholder {
1400  return @"";
1401 }
1402 
1403 - (void)removeDictationResultPlaceholder:(id)placeholder willInsertResult:(BOOL)willInsertResult {
1404 }
1405 
1406 - (NSString*)textInRange:(UITextRange*)range {
1407  if (!range) {
1408  return nil;
1409  }
1410  NSAssert([range isKindOfClass:[FlutterTextRange class]],
1411  @"Expected a FlutterTextRange for range (got %@).", [range class]);
1412  NSRange textRange = ((FlutterTextRange*)range).range;
1413  if (textRange.location == NSNotFound) {
1414  // Avoids [crashes](https://github.com/flutter/flutter/issues/138464) from an assertion
1415  // against NSNotFound.
1416  // TODO(hellohuanlin): This is a temp workaround, but we should look into why
1417  // framework is providing NSNotFound to the engine.
1418  // https://github.com/flutter/flutter/issues/160100
1419  return nil;
1420  }
1421  // Sanitize the range to prevent going out of bounds.
1422  NSUInteger location = MIN(textRange.location, self.text.length);
1423  NSUInteger length = MIN(self.text.length - location, textRange.length);
1424  NSRange safeRange = NSMakeRange(location, length);
1425  return [self.text substringWithRange:safeRange];
1426 }
1427 
1428 // Replace the text within the specified range with the given text,
1429 // without notifying the framework.
1430 - (void)replaceRangeLocal:(NSRange)range withText:(NSString*)text {
1431  [self.text replaceCharactersInRange:[self clampSelection:range forText:self.text]
1432  withString:text];
1433 
1434  // Adjust the selected range and the marked text range. There's no
1435  // documentation but UITextField always sets markedTextRange to nil,
1436  // and collapses the selection to the end of the new replacement text.
1437  const NSRange newSelectionRange =
1438  [self clampSelection:NSMakeRange(range.location + text.length, 0) forText:self.text];
1439 
1440  [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:newSelectionRange]];
1441  self.markedTextRange = nil;
1442 }
1443 
1444 - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
1445  NSString* textBeforeChange = [self.text copy];
1446  NSRange replaceRange = ((FlutterTextRange*)range).range;
1447  [self replaceRangeLocal:replaceRange withText:text];
1448  if (_enableDeltaModel) {
1449  NSRange nextReplaceRange = [self clampSelection:replaceRange forText:textBeforeChange];
1450  [self updateEditingStateWithDelta:flutter::TextEditingDelta(
1451  [textBeforeChange UTF8String],
1452  flutter::TextRange(
1453  nextReplaceRange.location,
1454  nextReplaceRange.location + nextReplaceRange.length),
1455  [text UTF8String])];
1456  } else {
1457  [self updateEditingState];
1458  }
1459 }
1460 
1461 - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
1462  // `temporarilyDeletedComposedCharacter` should only be used during a single text change session.
1463  // So it needs to be cleared at the start of each text editing session.
1464  self.temporarilyDeletedComposedCharacter = nil;
1465 
1466  if (self.returnKeyType == UIReturnKeyDefault && [text isEqualToString:@"\n"]) {
1467  [self.textInputDelegate flutterTextInputView:self
1468  performAction:FlutterTextInputActionNewline
1469  withClient:_textInputClient];
1470  return YES;
1471  }
1472 
1473  if ([text isEqualToString:@"\n"]) {
1474  FlutterTextInputAction action;
1475  switch (self.returnKeyType) {
1476  case UIReturnKeyDefault:
1477  action = FlutterTextInputActionUnspecified;
1478  break;
1479  case UIReturnKeyDone:
1480  action = FlutterTextInputActionDone;
1481  break;
1482  case UIReturnKeyGo:
1483  action = FlutterTextInputActionGo;
1484  break;
1485  case UIReturnKeySend:
1486  action = FlutterTextInputActionSend;
1487  break;
1488  case UIReturnKeySearch:
1489  case UIReturnKeyGoogle:
1490  case UIReturnKeyYahoo:
1491  action = FlutterTextInputActionSearch;
1492  break;
1493  case UIReturnKeyNext:
1494  action = FlutterTextInputActionNext;
1495  break;
1496  case UIReturnKeyContinue:
1497  action = FlutterTextInputActionContinue;
1498  break;
1499  case UIReturnKeyJoin:
1500  action = FlutterTextInputActionJoin;
1501  break;
1502  case UIReturnKeyRoute:
1503  action = FlutterTextInputActionRoute;
1504  break;
1505  case UIReturnKeyEmergencyCall:
1506  action = FlutterTextInputActionEmergencyCall;
1507  break;
1508  }
1509 
1510  [self.textInputDelegate flutterTextInputView:self
1511  performAction:action
1512  withClient:_textInputClient];
1513  return NO;
1514  }
1515 
1516  return YES;
1517 }
1518 
1519 // Either replaces the existing marked text or, if none is present, inserts it in
1520 // place of the current selection.
1521 - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelectedRange {
1522  NSString* textBeforeChange = [self.text copy];
1523 
1524  if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone ||
1525  _scribbleFocusStatus != FlutterScribbleFocusStatusUnfocused) {
1526  return;
1527  }
1528 
1529  if (markedText == nil) {
1530  markedText = @"";
1531  }
1532 
1533  const FlutterTextRange* currentMarkedTextRange = (FlutterTextRange*)self.markedTextRange;
1534  const NSRange& actualReplacedRange = currentMarkedTextRange && !currentMarkedTextRange.isEmpty
1535  ? currentMarkedTextRange.range
1537  // No need to call replaceRangeLocal as this method always adjusts the
1538  // selected/marked text ranges anyways.
1539  [self.text replaceCharactersInRange:actualReplacedRange withString:markedText];
1540 
1541  const NSRange newMarkedRange = NSMakeRange(actualReplacedRange.location, markedText.length);
1542  self.markedTextRange =
1543  newMarkedRange.length > 0 ? [FlutterTextRange rangeWithNSRange:newMarkedRange] : nil;
1544 
1545  [self setSelectedTextRangeLocal:
1547  rangeWithNSRange:[self clampSelection:NSMakeRange(markedSelectedRange.location +
1548  newMarkedRange.location,
1549  markedSelectedRange.length)
1550  forText:self.text]]];
1551  if (_enableDeltaModel) {
1552  NSRange nextReplaceRange = [self clampSelection:actualReplacedRange forText:textBeforeChange];
1553  [self updateEditingStateWithDelta:flutter::TextEditingDelta(
1554  [textBeforeChange UTF8String],
1555  flutter::TextRange(
1556  nextReplaceRange.location,
1557  nextReplaceRange.location + nextReplaceRange.length),
1558  [markedText UTF8String])];
1559  } else {
1560  [self updateEditingState];
1561  }
1562 }
1563 
1564 - (void)unmarkText {
1565  if (!self.markedTextRange) {
1566  return;
1567  }
1568  self.markedTextRange = nil;
1569  if (_enableDeltaModel) {
1570  [self updateEditingStateWithDelta:flutter::TextEditingDelta([self.text UTF8String])];
1571  } else {
1572  [self updateEditingState];
1573  }
1574 }
1575 
1576 - (UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition
1577  toPosition:(UITextPosition*)toPosition {
1578  NSUInteger fromIndex = ((FlutterTextPosition*)fromPosition).index;
1579  NSUInteger toIndex = ((FlutterTextPosition*)toPosition).index;
1580  if (toIndex >= fromIndex) {
1581  return [FlutterTextRange rangeWithNSRange:NSMakeRange(fromIndex, toIndex - fromIndex)];
1582  } else {
1583  // toIndex can be smaller than fromIndex, because
1584  // UITextInputStringTokenizer does not handle CJK characters
1585  // well in some cases. See:
1586  // https://github.com/flutter/flutter/issues/58750#issuecomment-644469521
1587  // Swap fromPosition and toPosition to match the behavior of native
1588  // UITextViews.
1589  return [FlutterTextRange rangeWithNSRange:NSMakeRange(toIndex, fromIndex - toIndex)];
1590  }
1591 }
1592 
1593 - (NSUInteger)decrementOffsetPosition:(NSUInteger)position {
1594  return fml::RangeForCharacterAtIndex(self.text, MAX(0, position - 1)).location;
1595 }
1596 
1597 - (NSUInteger)incrementOffsetPosition:(NSUInteger)position {
1598  NSRange charRange = fml::RangeForCharacterAtIndex(self.text, position);
1599  return MIN(position + charRange.length, self.text.length);
1600 }
1601 
1602 - (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInteger)offset {
1603  NSUInteger offsetPosition = ((FlutterTextPosition*)position).index;
1604 
1605  NSInteger newLocation = (NSInteger)offsetPosition + offset;
1606  if (newLocation < 0 || newLocation > (NSInteger)self.text.length) {
1607  return nil;
1608  }
1609 
1610  if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone) {
1611  return [FlutterTextPosition positionWithIndex:newLocation];
1612  }
1613 
1614  if (offset >= 0) {
1615  for (NSInteger i = 0; i < offset && offsetPosition < self.text.length; ++i) {
1616  offsetPosition = [self incrementOffsetPosition:offsetPosition];
1617  }
1618  } else {
1619  for (NSInteger i = 0; i < ABS(offset) && offsetPosition > 0; ++i) {
1620  offsetPosition = [self decrementOffsetPosition:offsetPosition];
1621  }
1622  }
1623  return [FlutterTextPosition positionWithIndex:offsetPosition];
1624 }
1625 
1626 - (UITextPosition*)positionFromPosition:(UITextPosition*)position
1627  inDirection:(UITextLayoutDirection)direction
1628  offset:(NSInteger)offset {
1629  // TODO(cbracken) Add RTL handling.
1630  switch (direction) {
1631  case UITextLayoutDirectionLeft:
1632  case UITextLayoutDirectionUp:
1633  return [self positionFromPosition:position offset:offset * -1];
1634  case UITextLayoutDirectionRight:
1635  case UITextLayoutDirectionDown:
1636  return [self positionFromPosition:position offset:1];
1637  }
1638 }
1639 
1640 - (UITextPosition*)beginningOfDocument {
1641  return [FlutterTextPosition positionWithIndex:0 affinity:UITextStorageDirectionForward];
1642 }
1643 
1644 - (UITextPosition*)endOfDocument {
1645  return [FlutterTextPosition positionWithIndex:self.text.length
1646  affinity:UITextStorageDirectionBackward];
1647 }
1648 
1649 - (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other {
1650  NSUInteger positionIndex = ((FlutterTextPosition*)position).index;
1651  NSUInteger otherIndex = ((FlutterTextPosition*)other).index;
1652  if (positionIndex < otherIndex) {
1653  return NSOrderedAscending;
1654  }
1655  if (positionIndex > otherIndex) {
1656  return NSOrderedDescending;
1657  }
1658  UITextStorageDirection positionAffinity = ((FlutterTextPosition*)position).affinity;
1659  UITextStorageDirection otherAffinity = ((FlutterTextPosition*)other).affinity;
1660  if (positionAffinity == otherAffinity) {
1661  return NSOrderedSame;
1662  }
1663  if (positionAffinity == UITextStorageDirectionBackward) {
1664  // positionAffinity points backwards, otherAffinity points forwards
1665  return NSOrderedAscending;
1666  }
1667  // positionAffinity points forwards, otherAffinity points backwards
1668  return NSOrderedDescending;
1669 }
1670 
1671 - (NSInteger)offsetFromPosition:(UITextPosition*)from toPosition:(UITextPosition*)toPosition {
1672  return ((FlutterTextPosition*)toPosition).index - ((FlutterTextPosition*)from).index;
1673 }
1674 
1675 - (UITextPosition*)positionWithinRange:(UITextRange*)range
1676  farthestInDirection:(UITextLayoutDirection)direction {
1677  NSUInteger index;
1678  UITextStorageDirection affinity;
1679  switch (direction) {
1680  case UITextLayoutDirectionLeft:
1681  case UITextLayoutDirectionUp:
1682  index = ((FlutterTextPosition*)range.start).index;
1683  affinity = UITextStorageDirectionForward;
1684  break;
1685  case UITextLayoutDirectionRight:
1686  case UITextLayoutDirectionDown:
1687  index = ((FlutterTextPosition*)range.end).index;
1688  affinity = UITextStorageDirectionBackward;
1689  break;
1690  }
1691  return [FlutterTextPosition positionWithIndex:index affinity:affinity];
1692 }
1693 
1694 - (UITextRange*)characterRangeByExtendingPosition:(UITextPosition*)position
1695  inDirection:(UITextLayoutDirection)direction {
1696  NSUInteger positionIndex = ((FlutterTextPosition*)position).index;
1697  NSUInteger startIndex;
1698  NSUInteger endIndex;
1699  switch (direction) {
1700  case UITextLayoutDirectionLeft:
1701  case UITextLayoutDirectionUp:
1702  startIndex = [self decrementOffsetPosition:positionIndex];
1703  endIndex = positionIndex;
1704  break;
1705  case UITextLayoutDirectionRight:
1706  case UITextLayoutDirectionDown:
1707  startIndex = positionIndex;
1708  endIndex = [self incrementOffsetPosition:positionIndex];
1709  break;
1710  }
1711  return [FlutterTextRange rangeWithNSRange:NSMakeRange(startIndex, endIndex - startIndex)];
1712 }
1713 
1714 #pragma mark - UITextInput text direction handling
1715 
1716 - (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position
1717  inDirection:(UITextStorageDirection)direction {
1718  // TODO(cbracken) Add RTL handling.
1719  return UITextWritingDirectionNatural;
1720 }
1721 
1722 - (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection
1723  forRange:(UITextRange*)range {
1724  // TODO(cbracken) Add RTL handling.
1725 }
1726 
1727 #pragma mark - UITextInput cursor, selection rect handling
1728 
1729 - (void)setMarkedRect:(CGRect)markedRect {
1730  _markedRect = markedRect;
1731  // Invalidate the cache.
1733 }
1734 
1735 // This method expects a 4x4 perspective matrix
1736 // stored in a NSArray in column-major order.
1737 - (void)setEditableTransform:(NSArray*)matrix {
1738  CATransform3D* transform = &_editableTransform;
1739 
1740  transform->m11 = [matrix[0] doubleValue];
1741  transform->m12 = [matrix[1] doubleValue];
1742  transform->m13 = [matrix[2] doubleValue];
1743  transform->m14 = [matrix[3] doubleValue];
1744 
1745  transform->m21 = [matrix[4] doubleValue];
1746  transform->m22 = [matrix[5] doubleValue];
1747  transform->m23 = [matrix[6] doubleValue];
1748  transform->m24 = [matrix[7] doubleValue];
1749 
1750  transform->m31 = [matrix[8] doubleValue];
1751  transform->m32 = [matrix[9] doubleValue];
1752  transform->m33 = [matrix[10] doubleValue];
1753  transform->m34 = [matrix[11] doubleValue];
1754 
1755  transform->m41 = [matrix[12] doubleValue];
1756  transform->m42 = [matrix[13] doubleValue];
1757  transform->m43 = [matrix[14] doubleValue];
1758  transform->m44 = [matrix[15] doubleValue];
1759 
1760  // Invalidate the cache.
1762 }
1763 
1764 // Returns the bounding CGRect of the transformed incomingRect, in the view's
1765 // coordinates.
1766 - (CGRect)localRectFromFrameworkTransform:(CGRect)incomingRect {
1767  CGPoint points[] = {
1768  incomingRect.origin,
1769  CGPointMake(incomingRect.origin.x, incomingRect.origin.y + incomingRect.size.height),
1770  CGPointMake(incomingRect.origin.x + incomingRect.size.width, incomingRect.origin.y),
1771  CGPointMake(incomingRect.origin.x + incomingRect.size.width,
1772  incomingRect.origin.y + incomingRect.size.height)};
1773 
1774  CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
1775  CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
1776 
1777  for (int i = 0; i < 4; i++) {
1778  const CGPoint point = points[i];
1779 
1780  CGFloat x = _editableTransform.m11 * point.x + _editableTransform.m21 * point.y +
1781  _editableTransform.m41;
1782  CGFloat y = _editableTransform.m12 * point.x + _editableTransform.m22 * point.y +
1783  _editableTransform.m42;
1784 
1785  const CGFloat w = _editableTransform.m14 * point.x + _editableTransform.m24 * point.y +
1786  _editableTransform.m44;
1787 
1788  if (w == 0.0) {
1789  return kInvalidFirstRect;
1790  } else if (w != 1.0) {
1791  x /= w;
1792  y /= w;
1793  }
1794 
1795  origin.x = MIN(origin.x, x);
1796  origin.y = MIN(origin.y, y);
1797  farthest.x = MAX(farthest.x, x);
1798  farthest.y = MAX(farthest.y, y);
1799  }
1800  return CGRectMake(origin.x, origin.y, farthest.x - origin.x, farthest.y - origin.y);
1801 }
1802 
1803 // The following methods are required to support force-touch cursor positioning
1804 // and to position the
1805 // candidates view for multi-stage input methods (e.g., Japanese) when using a
1806 // physical keyboard.
1807 // Returns the rect for the queried range, or a subrange through the end of line, if
1808 // the range encompasses multiple lines.
1809 - (CGRect)firstRectForRange:(UITextRange*)range {
1810  NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
1811  @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
1812  NSAssert([range.end isKindOfClass:[FlutterTextPosition class]],
1813  @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]);
1814  NSUInteger start = ((FlutterTextPosition*)range.start).index;
1815  NSUInteger end = ((FlutterTextPosition*)range.end).index;
1816  if (_markedTextRange != nil) {
1817  // The candidates view can't be shown if the framework has not sent the
1818  // first caret rect.
1819  if (CGRectEqualToRect(kInvalidFirstRect, _markedRect)) {
1820  return kInvalidFirstRect;
1821  }
1822 
1823  if (CGRectEqualToRect(_cachedFirstRect, kInvalidFirstRect)) {
1824  // If the width returned is too small, that means the framework sent us
1825  // the caret rect instead of the marked text rect. Expand it to 0.2 so
1826  // the IME candidates view would show up.
1827  CGRect rect = _markedRect;
1828  if (CGRectIsEmpty(rect)) {
1829  rect = CGRectInset(rect, -0.1, 0);
1830  }
1831  _cachedFirstRect = [self localRectFromFrameworkTransform:rect];
1832  }
1833 
1834  UIView* hostView = _textInputPlugin.hostView;
1835  NSAssert(hostView == nil || [self isDescendantOfView:hostView], @"%@ is not a descendant of %@",
1836  self, hostView);
1837  return hostView ? [hostView convertRect:_cachedFirstRect toView:self] : _cachedFirstRect;
1838  }
1839 
1840  if (_scribbleInteractionStatus == FlutterScribbleInteractionStatusNone &&
1841  _scribbleFocusStatus == FlutterScribbleFocusStatusUnfocused) {
1842  if (@available(iOS 17.0, *)) {
1843  // Disable auto-correction highlight feature for iOS 17+.
1844  // In iOS 17+, whenever a character is inserted or deleted, the system will always query
1845  // the rect for every single character of the current word.
1846  // GitHub Issue: https://github.com/flutter/flutter/issues/128406
1847  } else {
1848  // This tells the framework to show the highlight for incorrectly spelled word that is
1849  // about to be auto-corrected.
1850  // There is no other UITextInput API that informs about the auto-correction highlight.
1851  // So we simply add the call here as a workaround.
1852  [self.textInputDelegate flutterTextInputView:self
1853  showAutocorrectionPromptRectForStart:start
1854  end:end
1855  withClient:_textInputClient];
1856  }
1857  }
1858 
1859  // The iOS 16 system highlight does not repect the height returned by `firstRectForRange`
1860  // API (unlike iOS 17). So we return CGRectZero to hide it (unless if scribble is enabled).
1861  // To support scribble's advanced gestures (e.g. insert a space with a vertical bar),
1862  // at least 1 character's width is required.
1863  if (@available(iOS 17, *)) {
1864  // No-op
1865  } else if (![self isScribbleAvailable]) {
1866  return CGRectZero;
1867  }
1868 
1869  NSUInteger first = start;
1870  if (end < start) {
1871  first = end;
1872  }
1873 
1874  CGRect startSelectionRect = CGRectNull;
1875  CGRect endSelectionRect = CGRectNull;
1876  // Selection rects from different langauges may have different minY/maxY.
1877  // So we need to iterate through each rects to update minY/maxY.
1878  CGFloat minY = CGFLOAT_MAX;
1879  CGFloat maxY = CGFLOAT_MIN;
1880 
1881  FlutterTextRange* textRange = [FlutterTextRange
1882  rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1883  for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1884  BOOL startsOnOrBeforeStartOfRange = _selectionRects[i].position <= first;
1885  BOOL isLastSelectionRect = i + 1 == [_selectionRects count];
1886  BOOL endOfTextIsAfterStartOfRange = isLastSelectionRect && textRange.range.length > first;
1887  BOOL nextSelectionRectIsAfterStartOfRange =
1888  !isLastSelectionRect && _selectionRects[i + 1].position > first;
1889  if (startsOnOrBeforeStartOfRange &&
1890  (endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
1891  // TODO(hellohaunlin): Remove iOS 17 check. The logic should also work for older versions.
1892  if (@available(iOS 17, *)) {
1893  startSelectionRect = _selectionRects[i].rect;
1894  } else {
1895  return _selectionRects[i].rect;
1896  }
1897  }
1898  if (!CGRectIsNull(startSelectionRect)) {
1899  minY = fmin(minY, CGRectGetMinY(_selectionRects[i].rect));
1900  maxY = fmax(maxY, CGRectGetMaxY(_selectionRects[i].rect));
1901  BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1; // end is exclusive
1902  BOOL nextSelectionRectIsOnNextLine =
1903  !isLastSelectionRect &&
1904  // Selection rects from different langauges in 2 lines may overlap with each other.
1905  // A good approximation is to check if the center of next rect is below the bottom of
1906  // current rect.
1907  // TODO(hellohuanlin): Consider passing the line break info from framework.
1908  CGRectGetMidY(_selectionRects[i + 1].rect) > CGRectGetMaxY(_selectionRects[i].rect);
1909  if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectionRectIsOnNextLine) {
1910  endSelectionRect = _selectionRects[i].rect;
1911  break;
1912  }
1913  }
1914  }
1915  if (CGRectIsNull(startSelectionRect) || CGRectIsNull(endSelectionRect)) {
1916  return CGRectZero;
1917  } else {
1918  // fmin/fmax to support both LTR and RTL languages.
1919  CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
1920  CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
1921  return CGRectMake(minX, minY, maxX - minX, maxY - minY);
1922  }
1923 }
1924 
1925 - (CGRect)caretRectForPosition:(UITextPosition*)position {
1926  NSInteger index = ((FlutterTextPosition*)position).index;
1927  UITextStorageDirection affinity = ((FlutterTextPosition*)position).affinity;
1928  // Get the selectionRect of the characters before and after the requested caret position.
1929  NSArray<UITextSelectionRect*>* rects = [self
1930  selectionRectsForRange:[FlutterTextRange
1931  rangeWithNSRange:fml::RangeForCharactersInRange(
1932  self.text,
1933  NSMakeRange(
1934  MAX(0, index - 1),
1935  (index >= (NSInteger)self.text.length)
1936  ? 1
1937  : 2))]];
1938  if (rects.count == 0) {
1939  return CGRectZero;
1940  }
1941  if (index == 0) {
1942  // There is no character before the caret, so this will be the bounds of the character after the
1943  // caret position.
1944  CGRect characterAfterCaret = rects[0].rect;
1945  // Return a zero-width rectangle along the upstream edge of the character after the caret
1946  // position.
1947  if ([rects[0] isKindOfClass:[FlutterTextSelectionRect class]] &&
1948  ((FlutterTextSelectionRect*)rects[0]).isRTL) {
1949  return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1950  characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1951  } else {
1952  return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1953  characterAfterCaret.size.height);
1954  }
1955  } else if (rects.count == 2 && affinity == UITextStorageDirectionForward) {
1956  // There are characters before and after the caret, with forward direction affinity.
1957  // It's better to use the character after the caret.
1958  CGRect characterAfterCaret = rects[1].rect;
1959  // Return a zero-width rectangle along the upstream edge of the character after the caret
1960  // position.
1961  if ([rects[1] isKindOfClass:[FlutterTextSelectionRect class]] &&
1962  ((FlutterTextSelectionRect*)rects[1]).isRTL) {
1963  return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1964  characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1965  } else {
1966  return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1967  characterAfterCaret.size.height);
1968  }
1969  }
1970 
1971  // Covers 2 remaining cases:
1972  // 1. there are characters before and after the caret, with backward direction affinity.
1973  // 2. there is only 1 character before the caret (caret is at the end of text).
1974  // For both cases, return a zero-width rectangle along the downstream edge of the character
1975  // before the caret position.
1976  CGRect characterBeforeCaret = rects[0].rect;
1977  if ([rects[0] isKindOfClass:[FlutterTextSelectionRect class]] &&
1978  ((FlutterTextSelectionRect*)rects[0]).isRTL) {
1979  return CGRectMake(characterBeforeCaret.origin.x, characterBeforeCaret.origin.y, 0,
1980  characterBeforeCaret.size.height);
1981  } else {
1982  return CGRectMake(characterBeforeCaret.origin.x + characterBeforeCaret.size.width,
1983  characterBeforeCaret.origin.y, 0, characterBeforeCaret.size.height);
1984  }
1985 }
1986 
1987 - (UITextPosition*)closestPositionToPoint:(CGPoint)point {
1988  if ([_selectionRects count] == 0) {
1989  NSAssert([_selectedTextRange.start isKindOfClass:[FlutterTextPosition class]],
1990  @"Expected a FlutterTextPosition for position (got %@).",
1991  [_selectedTextRange.start class]);
1992  NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index;
1993  UITextStorageDirection currentAffinity =
1994  ((FlutterTextPosition*)_selectedTextRange.start).affinity;
1995  return [FlutterTextPosition positionWithIndex:currentIndex affinity:currentAffinity];
1996  }
1997 
1999  rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
2000  return [self closestPositionToPoint:point withinRange:range];
2001 }
2002 
2003 - (NSArray*)selectionRectsForRange:(UITextRange*)range {
2004  // At least in the simulator, swapping to the Japanese keyboard crashes the app as this method
2005  // is called immediately with a UITextRange with a UITextPosition rather than FlutterTextPosition
2006  // for the start and end.
2007  if (![range.start isKindOfClass:[FlutterTextPosition class]]) {
2008  return @[];
2009  }
2010  NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
2011  @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
2012  NSAssert([range.end isKindOfClass:[FlutterTextPosition class]],
2013  @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]);
2014  NSUInteger start = ((FlutterTextPosition*)range.start).index;
2015  NSUInteger end = ((FlutterTextPosition*)range.end).index;
2016  NSMutableArray* rects = [[NSMutableArray alloc] init];
2017  for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2018  if (_selectionRects[i].position >= start &&
2019  (_selectionRects[i].position < end ||
2020  (start == end && _selectionRects[i].position <= end))) {
2021  float width = _selectionRects[i].rect.size.width;
2022  if (start == end) {
2023  width = 0;
2024  }
2025  CGRect rect = CGRectMake(_selectionRects[i].rect.origin.x, _selectionRects[i].rect.origin.y,
2026  width, _selectionRects[i].rect.size.height);
2029  position:_selectionRects[i].position
2030  writingDirection:NSWritingDirectionNatural
2031  containsStart:(i == 0)
2032  containsEnd:(i == fml::RangeForCharactersInRange(
2033  self.text, NSMakeRange(0, self.text.length))
2034  .length)
2035  isVertical:NO];
2036  [rects addObject:selectionRect];
2037  }
2038  }
2039  return rects;
2040 }
2041 
2042 - (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range {
2043  NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
2044  @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
2045  NSAssert([range.end isKindOfClass:[FlutterTextPosition class]],
2046  @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]);
2047  NSUInteger start = ((FlutterTextPosition*)range.start).index;
2048  NSUInteger end = ((FlutterTextPosition*)range.end).index;
2049 
2050  // Selecting text using the floating cursor is not as precise as the pencil.
2051  // Allow further vertical deviation and base more of the decision on horizontal comparison.
2052  CGFloat verticalPrecision = _isFloatingCursorActive ? 10 : 1;
2053 
2054  // Find the selectionRect with a leading-center point that is closest to a given point.
2055  BOOL isFirst = YES;
2056  NSUInteger _closestRectIndex = 0;
2057  for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2058  NSUInteger position = _selectionRects[i].position;
2059  if (position >= start && position <= end) {
2060  if (isFirst ||
2062  point, _selectionRects[i].rect, _selectionRects[i].isRTL,
2063  /*useTrailingBoundaryOfSelectionRect=*/NO, _selectionRects[_closestRectIndex].rect,
2064  _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
2065  isFirst = NO;
2066  _closestRectIndex = i;
2067  }
2068  }
2069  }
2070 
2071  FlutterTextPosition* closestPosition =
2072  [FlutterTextPosition positionWithIndex:_selectionRects[_closestRectIndex].position
2073  affinity:UITextStorageDirectionForward];
2074 
2075  // Check if the far side of the closest rect is a better fit (e.g. tapping end of line)
2076  // Cannot simply check the _closestRectIndex result from the previous for loop due to RTL
2077  // writing direction and the gaps between selectionRects. So we also need to consider
2078  // the adjacent selectionRects to refine _closestRectIndex.
2079  for (NSUInteger i = MAX(0, _closestRectIndex - 1);
2080  i < MIN(_closestRectIndex + 2, [_selectionRects count]); i++) {
2081  NSUInteger position = _selectionRects[i].position + 1;
2082  if (position >= start && position <= end) {
2084  point, _selectionRects[i].rect, _selectionRects[i].isRTL,
2085  /*useTrailingBoundaryOfSelectionRect=*/YES, _selectionRects[_closestRectIndex].rect,
2086  _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
2087  // This is an upstream position
2088  closestPosition = [FlutterTextPosition positionWithIndex:position
2089  affinity:UITextStorageDirectionBackward];
2090  }
2091  }
2092  }
2093 
2094  return closestPosition;
2095 }
2096 
2097 - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
2098  // TODO(cbracken) Implement.
2099  NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index;
2100  return [FlutterTextRange rangeWithNSRange:fml::RangeForCharacterAtIndex(self.text, currentIndex)];
2101 }
2102 
2103 // Overall logic for floating cursor's "move" gesture and "selection" gesture:
2104 //
2105 // Floating cursor's "move" gesture takes 1 finger to force press the space bar, and then move the
2106 // cursor. The process starts with `beginFloatingCursorAtPoint`. When the finger is moved,
2107 // `updateFloatingCursorAtPoint` will be called. When the finger is released, `endFloatingCursor`
2108 // will be called. In all cases, we send the point (relative to the initial point registered in
2109 // beginFloatingCursorAtPoint) to the framework, so that framework can animate the floating cursor.
2110 //
2111 // During the move gesture, the framework only animate the cursor visually. It's only
2112 // after the gesture is complete, will the framework update the selection to the cursor's
2113 // new position (with zero selection length). This means during the animation, the visual effect
2114 // of the cursor is temporarily out of sync with the selection state in both framework and engine.
2115 // But it will be in sync again after the animation is complete.
2116 //
2117 // Floating cursor's "selection" gesture also starts with 1 finger to force press the space bar,
2118 // so exactly the same functions as the "move gesture" discussed above will be called. When the
2119 // second finger is pressed, `setSelectedText` will be called. This mechanism requires
2120 // `closestPositionFromPoint` to be implemented, to allow UIKit to translate the finger touch
2121 // location displacement to the text range to select. When the selection is completed
2122 // (i.e. when both of the 2 fingers are released), similar to "move" gesture,
2123 // the `endFloatingCursor` will be called.
2124 //
2125 // When the 2nd finger is pressed, it does not trigger another startFloatingCursor call. So
2126 // floating cursor move/selection logic has to be implemented in iOS embedder rather than
2127 // just the framework side.
2128 //
2129 // Whenever a selection is updated, the engine sends the new selection to the framework. So unlike
2130 // the move gesture, the selections in the framework and the engine are always kept in sync.
2131 - (void)beginFloatingCursorAtPoint:(CGPoint)point {
2132  // For "beginFloatingCursorAtPoint" and "updateFloatingCursorAtPoint", "point" is roughly:
2133  //
2134  // CGPoint(
2135  // width >= 0 ? point.x.clamp(boundingBox.left, boundingBox.right) : point.x,
2136  // height >= 0 ? point.y.clamp(boundingBox.top, boundingBox.bottom) : point.y,
2137  // )
2138  // where
2139  // point = keyboardPanGestureRecognizer.translationInView(textInputView) + caretRectForPosition
2140  // boundingBox = self.convertRect(bounds, fromView:textInputView)
2141  // bounds = self._selectionClipRect ?? self.bounds
2142  //
2143  // It seems impossible to use a negative "width" or "height", as the "convertRect"
2144  // call always turns a CGRect's negative dimensions into non-negative values, e.g.,
2145  // (1, 2, -3, -4) would become (-2, -2, 3, 4).
2147  _floatingCursorOffset = point;
2148  [self.textInputDelegate flutterTextInputView:self
2149  updateFloatingCursor:FlutterFloatingCursorDragStateStart
2150  withClient:_textInputClient
2151  withPosition:@{@"X" : @0, @"Y" : @0}];
2152 }
2153 
2154 - (void)updateFloatingCursorAtPoint:(CGPoint)point {
2155  [self.textInputDelegate flutterTextInputView:self
2156  updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2157  withClient:_textInputClient
2158  withPosition:@{
2159  @"X" : @(point.x - _floatingCursorOffset.x),
2160  @"Y" : @(point.y - _floatingCursorOffset.y)
2161  }];
2162 }
2163 
2164 - (void)endFloatingCursor {
2166  [self.textInputDelegate flutterTextInputView:self
2167  updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2168  withClient:_textInputClient
2169  withPosition:@{@"X" : @0, @"Y" : @0}];
2170 }
2171 
2172 #pragma mark - UIKeyInput Overrides
2173 
2174 - (void)updateEditingState {
2175  NSUInteger selectionBase = ((FlutterTextPosition*)_selectedTextRange.start).index;
2176  NSUInteger selectionExtent = ((FlutterTextPosition*)_selectedTextRange.end).index;
2177 
2178  // Empty compositing range is represented by the framework's TextRange.empty.
2179  NSInteger composingBase = -1;
2180  NSInteger composingExtent = -1;
2181  if (self.markedTextRange != nil) {
2182  composingBase = ((FlutterTextPosition*)self.markedTextRange.start).index;
2183  composingExtent = ((FlutterTextPosition*)self.markedTextRange.end).index;
2184  }
2185  NSDictionary* state = @{
2186  @"selectionBase" : @(selectionBase),
2187  @"selectionExtent" : @(selectionExtent),
2188  @"selectionAffinity" : @(_selectionAffinity),
2189  @"selectionIsDirectional" : @(false),
2190  @"composingBase" : @(composingBase),
2191  @"composingExtent" : @(composingExtent),
2192  @"text" : [NSString stringWithString:self.text],
2193  };
2194 
2195  if (_textInputClient == 0 && _autofillId != nil) {
2196  [self.textInputDelegate flutterTextInputView:self
2197  updateEditingClient:_textInputClient
2198  withState:state
2199  withTag:_autofillId];
2200  } else {
2201  [self.textInputDelegate flutterTextInputView:self
2202  updateEditingClient:_textInputClient
2203  withState:state];
2204  }
2205 }
2206 
2207 - (void)updateEditingStateWithDelta:(flutter::TextEditingDelta)delta {
2208  NSUInteger selectionBase = ((FlutterTextPosition*)_selectedTextRange.start).index;
2209  NSUInteger selectionExtent = ((FlutterTextPosition*)_selectedTextRange.end).index;
2210 
2211  // Empty compositing range is represented by the framework's TextRange.empty.
2212  NSInteger composingBase = -1;
2213  NSInteger composingExtent = -1;
2214  if (self.markedTextRange != nil) {
2215  composingBase = ((FlutterTextPosition*)self.markedTextRange.start).index;
2216  composingExtent = ((FlutterTextPosition*)self.markedTextRange.end).index;
2217  }
2218 
2219  NSDictionary* deltaToFramework = @{
2220  @"oldText" : @(delta.old_text().c_str()),
2221  @"deltaText" : @(delta.delta_text().c_str()),
2222  @"deltaStart" : @(delta.delta_start()),
2223  @"deltaEnd" : @(delta.delta_end()),
2224  @"selectionBase" : @(selectionBase),
2225  @"selectionExtent" : @(selectionExtent),
2226  @"selectionAffinity" : @(_selectionAffinity),
2227  @"selectionIsDirectional" : @(false),
2228  @"composingBase" : @(composingBase),
2229  @"composingExtent" : @(composingExtent),
2230  };
2231 
2232  [_pendingDeltas addObject:deltaToFramework];
2233 
2234  if (_pendingDeltas.count == 1) {
2235  __weak FlutterTextInputView* weakSelf = self;
2236  dispatch_async(dispatch_get_main_queue(), ^{
2237  __strong FlutterTextInputView* strongSelf = weakSelf;
2238  if (strongSelf && strongSelf.pendingDeltas.count > 0) {
2239  NSDictionary* deltas = @{
2240  @"deltas" : strongSelf.pendingDeltas,
2241  };
2242 
2243  [strongSelf.textInputDelegate flutterTextInputView:strongSelf
2244  updateEditingClient:strongSelf->_textInputClient
2245  withDelta:deltas];
2246  [strongSelf.pendingDeltas removeAllObjects];
2247  }
2248  });
2249  }
2250 }
2251 
2252 - (BOOL)hasText {
2253  return self.text.length > 0;
2254 }
2255 
2256 - (void)insertText:(NSString*)text {
2257  if (self.temporarilyDeletedComposedCharacter.length > 0 && text.length == 1 && !text.UTF8String &&
2258  [text characterAtIndex:0] == [self.temporarilyDeletedComposedCharacter characterAtIndex:0]) {
2259  // Workaround for https://github.com/flutter/flutter/issues/111494
2260  // TODO(cyanglaz): revert this workaround if when flutter supports a minimum iOS version which
2261  // this bug is fixed by Apple.
2262  text = self.temporarilyDeletedComposedCharacter;
2263  self.temporarilyDeletedComposedCharacter = nil;
2264  }
2265 
2266  NSMutableArray<FlutterTextSelectionRect*>* copiedRects =
2267  [[NSMutableArray alloc] initWithCapacity:[_selectionRects count]];
2268  NSAssert([_selectedTextRange.start isKindOfClass:[FlutterTextPosition class]],
2269  @"Expected a FlutterTextPosition for position (got %@).",
2270  [_selectedTextRange.start class]);
2271  NSUInteger insertPosition = ((FlutterTextPosition*)_selectedTextRange.start).index;
2272  for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2273  NSUInteger rectPosition = _selectionRects[i].position;
2274  if (rectPosition == insertPosition) {
2275  for (NSUInteger j = 0; j <= text.length; j++) {
2276  [copiedRects addObject:[FlutterTextSelectionRect
2277  selectionRectWithRect:_selectionRects[i].rect
2278  position:rectPosition + j
2279  writingDirection:_selectionRects[i].writingDirection]];
2280  }
2281  } else {
2282  if (rectPosition > insertPosition) {
2283  rectPosition = rectPosition + text.length;
2284  }
2285  [copiedRects addObject:[FlutterTextSelectionRect
2286  selectionRectWithRect:_selectionRects[i].rect
2287  position:rectPosition
2288  writingDirection:_selectionRects[i].writingDirection]];
2289  }
2290  }
2291 
2292  _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2293  [self resetScribbleInteractionStatusIfEnding];
2294  self.selectionRects = copiedRects;
2296  [self replaceRange:_selectedTextRange withText:text];
2297 }
2298 
2299 - (UITextPlaceholder*)insertTextPlaceholderWithSize:(CGSize)size API_AVAILABLE(ios(13.0)) {
2300  [self.textInputDelegate flutterTextInputView:self
2301  insertTextPlaceholderWithSize:size
2302  withClient:_textInputClient];
2303  _hasPlaceholder = YES;
2304  return [[FlutterTextPlaceholder alloc] init];
2305 }
2306 
2307 - (void)removeTextPlaceholder:(UITextPlaceholder*)textPlaceholder API_AVAILABLE(ios(13.0)) {
2308  _hasPlaceholder = NO;
2309  [self.textInputDelegate flutterTextInputView:self removeTextPlaceholder:_textInputClient];
2310 }
2311 
2312 - (void)deleteBackward {
2314  _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2315  [self resetScribbleInteractionStatusIfEnding];
2316 
2317  // When deleting Thai vowel, _selectedTextRange has location
2318  // but does not have length, so we have to manually set it.
2319  // In addition, we needed to delete only a part of grapheme cluster
2320  // because it is the expected behavior of Thai input.
2321  // https://github.com/flutter/flutter/issues/24203
2322  // https://github.com/flutter/flutter/issues/21745
2323  // https://github.com/flutter/flutter/issues/39399
2324  //
2325  // This is needed for correct handling of the deletion of Thai vowel input.
2326  // TODO(cbracken): Get a good understanding of expected behavior of Thai
2327  // input and ensure that this is the correct solution.
2328  // https://github.com/flutter/flutter/issues/28962
2329  if (_selectedTextRange.isEmpty && [self hasText]) {
2330  UITextRange* oldSelectedRange = _selectedTextRange;
2331  NSRange oldRange = ((FlutterTextRange*)oldSelectedRange).range;
2332  if (oldRange.location > 0) {
2333  NSRange newRange = NSMakeRange(oldRange.location - 1, 1);
2334 
2335  // We should check if the last character is a part of emoji.
2336  // If so, we must delete the entire emoji to prevent the text from being malformed.
2337  NSRange charRange = fml::RangeForCharacterAtIndex(self.text, oldRange.location - 1);
2338  if (IsEmoji(self.text, charRange)) {
2339  newRange = NSMakeRange(charRange.location, oldRange.location - charRange.location);
2340  }
2341 
2343  }
2344  }
2345 
2346  if (!_selectedTextRange.isEmpty) {
2347  // Cache the last deleted emoji to use for an iOS bug where the next
2348  // insertion corrupts the emoji characters.
2349  // See: https://github.com/flutter/flutter/issues/111494#issuecomment-1248441346
2350  if (IsEmoji(self.text, _selectedTextRange.range)) {
2351  NSString* deletedText = [self.text substringWithRange:_selectedTextRange.range];
2352  NSRange deleteFirstCharacterRange = fml::RangeForCharacterAtIndex(deletedText, 0);
2353  self.temporarilyDeletedComposedCharacter =
2354  [deletedText substringWithRange:deleteFirstCharacterRange];
2355  }
2356  [self replaceRange:_selectedTextRange withText:@""];
2357  }
2358 }
2359 
2360 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(id)target {
2361  UIAccessibilityPostNotification(notification, target);
2362 }
2363 
2364 - (void)accessibilityElementDidBecomeFocused {
2365  if ([self accessibilityElementIsFocused]) {
2366  // For most of the cases, this flutter text input view should never
2367  // receive the focus. If we do receive the focus, we make the best effort
2368  // to send the focus back to the real text field.
2369  FML_DCHECK(_backingTextInputAccessibilityObject);
2370  [self postAccessibilityNotification:UIAccessibilityScreenChangedNotification
2371  target:_backingTextInputAccessibilityObject];
2372  }
2373 }
2374 
2375 - (BOOL)accessibilityElementsHidden {
2376  return !_accessibilityEnabled;
2377 }
2378 
2380  if (_scribbleInteractionStatus == FlutterScribbleInteractionStatusEnding) {
2381  _scribbleInteractionStatus = FlutterScribbleInteractionStatusNone;
2382  }
2383 }
2384 
2385 #pragma mark - Key Events Handling
2386 - (void)pressesBegan:(NSSet<UIPress*>*)presses
2387  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2388  [_textInputPlugin.viewController pressesBegan:presses withEvent:event];
2389 }
2390 
2391 - (void)pressesChanged:(NSSet<UIPress*>*)presses
2392  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2393  [_textInputPlugin.viewController pressesChanged:presses withEvent:event];
2394 }
2395 
2396 - (void)pressesEnded:(NSSet<UIPress*>*)presses
2397  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2398  [_textInputPlugin.viewController pressesEnded:presses withEvent:event];
2399 }
2400 
2401 - (void)pressesCancelled:(NSSet<UIPress*>*)presses
2402  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2403  [_textInputPlugin.viewController pressesCancelled:presses withEvent:event];
2404 }
2405 
2406 @end
2407 
2408 /**
2409  * Hides `FlutterTextInputView` from iOS accessibility system so it
2410  * does not show up twice, once where it is in the `UIView` hierarchy,
2411  * and a second time as part of the `SemanticsObject` hierarchy.
2412  *
2413  * This prevents the `FlutterTextInputView` from receiving the focus
2414  * due to swiping gesture.
2415  *
2416  * There are other cases the `FlutterTextInputView` may receive
2417  * focus. One example is during screen changes, the accessibility
2418  * tree will undergo a dramatic structural update. The Voiceover may
2419  * decide to focus the `FlutterTextInputView` that is not involved
2420  * in the structural update instead. If that happens, the
2421  * `FlutterTextInputView` will make a best effort to direct the
2422  * focus back to the `SemanticsObject`.
2423  */
2425 }
2426 
2427 @end
2428 
2430 }
2431 
2432 - (BOOL)accessibilityElementsHidden {
2433  return YES;
2434 }
2435 
2436 @end
2437 
2438 @interface FlutterTextInputPlugin ()
2439 - (void)enableActiveViewAccessibility;
2440 @end
2441 
2442 @interface FlutterTimerProxy : NSObject
2443 @property(nonatomic, weak) FlutterTextInputPlugin* target;
2444 @end
2445 
2446 @implementation FlutterTimerProxy
2447 
2448 + (instancetype)proxyWithTarget:(FlutterTextInputPlugin*)target {
2449  FlutterTimerProxy* proxy = [[self alloc] init];
2450  if (proxy) {
2451  proxy.target = target;
2452  }
2453  return proxy;
2454 }
2455 
2456 - (void)enableActiveViewAccessibility {
2457  [self.target enableActiveViewAccessibility];
2458 }
2459 
2460 @end
2461 
2462 @interface FlutterTextInputPlugin ()
2463 // The current password-autofillable input fields that have yet to be saved.
2464 @property(nonatomic, readonly)
2465  NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
2466 @property(nonatomic, retain) FlutterTextInputView* activeView;
2467 @property(nonatomic, retain) FlutterTextInputViewAccessibilityHider* inputHider;
2468 @property(nonatomic, readonly, weak) id<FlutterViewResponder> viewResponder;
2469 
2470 @property(nonatomic, strong) UIView* keyboardViewContainer;
2471 @property(nonatomic, strong) UIView* keyboardView;
2472 @property(nonatomic, strong) UIView* cachedFirstResponder;
2473 @property(nonatomic, assign) CGRect keyboardRect;
2474 @property(nonatomic, assign) CGFloat previousPointerYPosition;
2475 @property(nonatomic, assign) CGFloat pointerYVelocity;
2476 @end
2477 
2478 @implementation FlutterTextInputPlugin {
2479  NSTimer* _enableFlutterTextInputViewAccessibilityTimer;
2480 }
2481 
2482 - (instancetype)initWithDelegate:(id<FlutterTextInputDelegate>)textInputDelegate {
2483  self = [super init];
2484  if (self) {
2485  // `_textInputDelegate` is a weak reference because it should retain FlutterTextInputPlugin.
2486  _textInputDelegate = textInputDelegate;
2487  _autofillContext = [[NSMutableDictionary alloc] init];
2488  _inputHider = [[FlutterTextInputViewAccessibilityHider alloc] init];
2489  _scribbleElements = [[NSMutableDictionary alloc] init];
2490  _keyboardViewContainer = [[UIView alloc] init];
2491 
2492  [[NSNotificationCenter defaultCenter] addObserver:self
2493  selector:@selector(handleKeyboardWillShow:)
2494  name:UIKeyboardWillShowNotification
2495  object:nil];
2496  }
2497 
2498  return self;
2499 }
2500 
2501 - (void)handleKeyboardWillShow:(NSNotification*)notification {
2502  NSDictionary* keyboardInfo = [notification userInfo];
2503  NSValue* keyboardFrameEnd = [keyboardInfo valueForKey:UIKeyboardFrameEndUserInfoKey];
2504  _keyboardRect = [keyboardFrameEnd CGRectValue];
2505 }
2506 
2507 - (void)dealloc {
2508  [self hideTextInput];
2509 }
2510 
2511 - (void)removeEnableFlutterTextInputViewAccessibilityTimer {
2512  if (_enableFlutterTextInputViewAccessibilityTimer) {
2513  [_enableFlutterTextInputViewAccessibilityTimer invalidate];
2514  _enableFlutterTextInputViewAccessibilityTimer = nil;
2515  }
2516 }
2517 
2518 - (UIView<UITextInput>*)textInputView {
2519  return _activeView;
2520 }
2521 
2522 - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
2523  NSString* method = call.method;
2524  id args = call.arguments;
2525  if ([method isEqualToString:kShowMethod]) {
2526  [self showTextInput];
2527  result(nil);
2528  } else if ([method isEqualToString:kHideMethod]) {
2529  [self hideTextInput];
2530  result(nil);
2531  } else if ([method isEqualToString:kSetClientMethod]) {
2532  [self setTextInputClient:[args[0] intValue] withConfiguration:args[1]];
2533  result(nil);
2534  } else if ([method isEqualToString:kSetPlatformViewClientMethod]) {
2535  // This method call has a `platformViewId` argument, but we do not need it for iOS for now.
2536  [self setPlatformViewTextInputClient];
2537  result(nil);
2538  } else if ([method isEqualToString:kSetEditingStateMethod]) {
2539  [self setTextInputEditingState:args];
2540  result(nil);
2541  } else if ([method isEqualToString:kClearClientMethod]) {
2542  [self clearTextInputClient];
2543  result(nil);
2544  } else if ([method isEqualToString:kSetEditableSizeAndTransformMethod]) {
2545  [self setEditableSizeAndTransform:args];
2546  result(nil);
2547  } else if ([method isEqualToString:kSetMarkedTextRectMethod]) {
2548  [self updateMarkedRect:args];
2549  result(nil);
2550  } else if ([method isEqualToString:kFinishAutofillContextMethod]) {
2551  [self triggerAutofillSave:[args boolValue]];
2552  result(nil);
2553  // TODO(justinmc): Remove the TextInput method constant when the framework has
2554  // finished transitioning to using the Scribble channel.
2555  // https://github.com/flutter/flutter/pull/104128
2556  } else if ([method isEqualToString:kDeprecatedSetSelectionRectsMethod]) {
2557  [self setSelectionRects:args];
2558  result(nil);
2559  } else if ([method isEqualToString:kSetSelectionRectsMethod]) {
2560  [self setSelectionRects:args];
2561  result(nil);
2562  } else if ([method isEqualToString:kStartLiveTextInputMethod]) {
2563  [self startLiveTextInput];
2564  result(nil);
2565  } else if ([method isEqualToString:kUpdateConfigMethod]) {
2566  [self updateConfig:args];
2567  result(nil);
2568  } else if ([method isEqualToString:kOnInteractiveKeyboardPointerMoveMethod]) {
2569  CGFloat pointerY = (CGFloat)[args[@"pointerY"] doubleValue];
2570  [self handlePointerMove:pointerY];
2571  result(nil);
2572  } else if ([method isEqualToString:kOnInteractiveKeyboardPointerUpMethod]) {
2573  CGFloat pointerY = (CGFloat)[args[@"pointerY"] doubleValue];
2574  [self handlePointerUp:pointerY];
2575  result(nil);
2576  } else {
2578  }
2579 }
2580 
2581 - (void)handlePointerUp:(CGFloat)pointerY {
2582  if (_keyboardView.superview != nil) {
2583  // Done to avoid the issue of a pointer up done without a screenshot
2584  // View must be loaded at this point.
2585  UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2586  CGFloat screenHeight = screen.bounds.size.height;
2587  CGFloat keyboardHeight = _keyboardRect.size.height;
2588  // Negative velocity indicates a downward movement
2589  BOOL shouldDismissKeyboardBasedOnVelocity = _pointerYVelocity < 0;
2590  [UIView animateWithDuration:kKeyboardAnimationTimeToCompleteion
2591  animations:^{
2592  double keyboardDestination =
2593  shouldDismissKeyboardBasedOnVelocity ? screenHeight : screenHeight - keyboardHeight;
2594  _keyboardViewContainer.frame = CGRectMake(
2595  0, keyboardDestination, _viewController.flutterScreenIfViewLoaded.bounds.size.width,
2596  _keyboardViewContainer.frame.size.height);
2597  }
2598  completion:^(BOOL finished) {
2599  if (shouldDismissKeyboardBasedOnVelocity) {
2600  [self.textInputDelegate flutterTextInputView:self.activeView
2601  didResignFirstResponderWithTextInputClient:self.activeView.textInputClient];
2602  [self dismissKeyboardScreenshot];
2603  } else {
2604  [self showKeyboardAndRemoveScreenshot];
2605  }
2606  }];
2607  }
2608 }
2609 
2610 - (void)dismissKeyboardScreenshot {
2611  for (UIView* subView in _keyboardViewContainer.subviews) {
2612  [subView removeFromSuperview];
2613  }
2614 }
2615 
2616 - (void)showKeyboardAndRemoveScreenshot {
2617  [UIView setAnimationsEnabled:NO];
2618  [_cachedFirstResponder becomeFirstResponder];
2619  // UIKit does not immediately access the areAnimationsEnabled Boolean so a delay is needed before
2620  // returned
2621  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kKeyboardAnimationDelaySeconds * NSEC_PER_SEC),
2622  dispatch_get_main_queue(), ^{
2623  [UIView setAnimationsEnabled:YES];
2624  [self dismissKeyboardScreenshot];
2625  });
2626 }
2627 
2628 - (void)handlePointerMove:(CGFloat)pointerY {
2629  // View must be loaded at this point.
2630  UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2631  CGFloat screenHeight = screen.bounds.size.height;
2632  CGFloat keyboardHeight = _keyboardRect.size.height;
2633  if (screenHeight - keyboardHeight <= pointerY) {
2634  // If the pointer is within the bounds of the keyboard.
2635  if (_keyboardView.superview == nil) {
2636  // If no screenshot has been taken.
2637  [self takeKeyboardScreenshotAndDisplay];
2638  [self hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate];
2639  } else {
2640  [self setKeyboardContainerHeight:pointerY];
2641  _pointerYVelocity = _previousPointerYPosition - pointerY;
2642  }
2643  } else {
2644  if (_keyboardView.superview != nil) {
2645  // Keeps keyboard at proper height.
2646  _keyboardViewContainer.frame = _keyboardRect;
2647  _pointerYVelocity = _previousPointerYPosition - pointerY;
2648  }
2649  }
2650  _previousPointerYPosition = pointerY;
2651 }
2652 
2653 - (void)setKeyboardContainerHeight:(CGFloat)pointerY {
2654  CGRect frameRect = _keyboardRect;
2655  frameRect.origin.y = pointerY;
2656  _keyboardViewContainer.frame = frameRect;
2657 }
2658 
2659 - (void)hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate {
2660  [UIView setAnimationsEnabled:NO];
2661  UIApplication* flutterApplication = FlutterSharedApplication.application;
2662  _cachedFirstResponder =
2663  flutterApplication
2664  ? flutterApplication.keyWindow.flutterFirstResponder
2665  : self.viewController.flutterWindowSceneIfViewLoaded.keyWindow.flutterFirstResponder;
2666 
2667  _activeView.preventCursorDismissWhenResignFirstResponder = YES;
2668  [_cachedFirstResponder resignFirstResponder];
2669  _activeView.preventCursorDismissWhenResignFirstResponder = NO;
2670  [UIView setAnimationsEnabled:YES];
2671 }
2672 
2673 - (void)takeKeyboardScreenshotAndDisplay {
2674  // View must be loaded at this point
2675  UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2676  UIView* keyboardSnap = [screen snapshotViewAfterScreenUpdates:YES];
2677  keyboardSnap = [keyboardSnap resizableSnapshotViewFromRect:_keyboardRect
2678  afterScreenUpdates:YES
2679  withCapInsets:UIEdgeInsetsZero];
2680  _keyboardView = keyboardSnap;
2681  [_keyboardViewContainer addSubview:_keyboardView];
2682  if (_keyboardViewContainer.superview == nil) {
2683  UIApplication* flutterApplication = FlutterSharedApplication.application;
2684  UIView* rootView = flutterApplication
2685  ? flutterApplication.delegate.window.rootViewController.view
2686  : self.viewController.viewIfLoaded.window.rootViewController.view;
2687  [rootView addSubview:_keyboardViewContainer];
2688  }
2689  _keyboardViewContainer.layer.zPosition = NSIntegerMax;
2690  _keyboardViewContainer.frame = _keyboardRect;
2691 }
2692 
2693 - (BOOL)showEditMenu:(NSDictionary*)args API_AVAILABLE(ios(16.0)) {
2694  if (!self.activeView.isFirstResponder) {
2695  return NO;
2696  }
2697  NSDictionary<NSString*, NSNumber*>* encodedTargetRect = args[@"targetRect"];
2698  CGRect globalTargetRect = CGRectMake(
2699  [encodedTargetRect[@"x"] doubleValue], [encodedTargetRect[@"y"] doubleValue],
2700  [encodedTargetRect[@"width"] doubleValue], [encodedTargetRect[@"height"] doubleValue]);
2701  CGRect localTargetRect = [self.hostView convertRect:globalTargetRect toView:self.activeView];
2702  [self.activeView showEditMenuWithTargetRect:localTargetRect items:args[@"items"]];
2703  return YES;
2704 }
2705 
2706 - (void)hideEditMenu {
2707  [self.activeView hideEditMenu];
2708 }
2709 
2710 - (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
2711  NSArray* transform = dictionary[@"transform"];
2712  [_activeView setEditableTransform:transform];
2713  const int leftIndex = 12;
2714  const int topIndex = 13;
2715  if ([_activeView isScribbleAvailable]) {
2716  // This is necessary to set up where the scribble interactable element will be.
2717  _inputHider.frame =
2718  CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue],
2719  [dictionary[@"width"] intValue], [dictionary[@"height"] intValue]);
2720  _activeView.frame =
2721  CGRectMake(0, 0, [dictionary[@"width"] intValue], [dictionary[@"height"] intValue]);
2722  _activeView.tintColor = [UIColor clearColor];
2723  } else {
2724  // TODO(hellohuanlin): Also need to handle iOS 16 case, where the auto-correction highlight does
2725  // not match the size of text.
2726  // See https://github.com/flutter/flutter/issues/131695
2727  if (@available(iOS 17, *)) {
2728  // Move auto-correction highlight to overlap with the actual text.
2729  // This is to fix an issue where the system auto-correction highlight is displayed at
2730  // the top left corner of the screen on iOS 17+.
2731  // This problem also happens on iOS 16, but the size of highlight does not match the text.
2732  // See https://github.com/flutter/flutter/issues/131695
2733  // TODO(hellohuanlin): Investigate if we can use non-zero size.
2734  _inputHider.frame =
2735  CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue], 0, 0);
2736  }
2737  }
2738 }
2739 
2740 - (void)updateMarkedRect:(NSDictionary*)dictionary {
2741  NSAssert(dictionary[@"x"] != nil && dictionary[@"y"] != nil && dictionary[@"width"] != nil &&
2742  dictionary[@"height"] != nil,
2743  @"Expected a dictionary representing a CGRect, got %@", dictionary);
2744  CGRect rect = CGRectMake([dictionary[@"x"] doubleValue], [dictionary[@"y"] doubleValue],
2745  [dictionary[@"width"] doubleValue], [dictionary[@"height"] doubleValue]);
2746  _activeView.markedRect = rect.size.width < 0 && rect.size.height < 0 ? kInvalidFirstRect : rect;
2747 }
2748 
2749 - (void)setSelectionRects:(NSArray*)encodedRects {
2750  NSMutableArray<FlutterTextSelectionRect*>* rectsAsRect =
2751  [[NSMutableArray alloc] initWithCapacity:[encodedRects count]];
2752  for (NSUInteger i = 0; i < [encodedRects count]; i++) {
2753  NSArray<NSNumber*>* encodedRect = encodedRects[i];
2754  [rectsAsRect addObject:[FlutterTextSelectionRect
2755  selectionRectWithRect:CGRectMake([encodedRect[0] floatValue],
2756  [encodedRect[1] floatValue],
2757  [encodedRect[2] floatValue],
2758  [encodedRect[3] floatValue])
2759  position:[encodedRect[4] unsignedIntegerValue]
2760  writingDirection:[encodedRect[5] unsignedIntegerValue] == 1
2761  ? NSWritingDirectionLeftToRight
2762  : NSWritingDirectionRightToLeft]];
2763  }
2764 
2765  // TODO(hellohuanlin): Investigate why notifying the text input system about text changes (via
2766  // textWillChange and textDidChange APIs) causes a bug where we cannot enter text with IME
2767  // keyboards. Issue: https://github.com/flutter/flutter/issues/133908
2768  _activeView.selectionRects = rectsAsRect;
2769 }
2770 
2771 - (void)startLiveTextInput {
2772  if (@available(iOS 15.0, *)) {
2773  if (_activeView == nil || !_activeView.isFirstResponder) {
2774  return;
2775  }
2776  [_activeView captureTextFromCamera:nil];
2777  }
2778 }
2779 
2780 - (void)showTextInput {
2781  _activeView.viewResponder = _viewResponder;
2782  [self addToInputParentViewIfNeeded:_activeView];
2783  // Adds a delay to prevent the text view from receiving accessibility
2784  // focus in case it is activated during semantics updates.
2785  //
2786  // One common case is when the app navigates to a page with an auto
2787  // focused text field. The text field will activate the FlutterTextInputView
2788  // with a semantics update sent to the engine. The voiceover will focus
2789  // the newly attached active view while performing accessibility update.
2790  // This results in accessibility focus stuck at the FlutterTextInputView.
2791  if (!_enableFlutterTextInputViewAccessibilityTimer) {
2792  _enableFlutterTextInputViewAccessibilityTimer =
2793  [NSTimer scheduledTimerWithTimeInterval:kUITextInputAccessibilityEnablingDelaySeconds
2794  target:[FlutterTimerProxy proxyWithTarget:self]
2795  selector:@selector(enableActiveViewAccessibility)
2796  userInfo:nil
2797  repeats:NO];
2798  }
2799  [_activeView becomeFirstResponder];
2800 }
2801 
2802 - (void)enableActiveViewAccessibility {
2803  if (_activeView.isFirstResponder) {
2804  _activeView.accessibilityEnabled = YES;
2805  }
2806  [self removeEnableFlutterTextInputViewAccessibilityTimer];
2807 }
2808 
2809 - (void)hideTextInput {
2810  [self removeEnableFlutterTextInputViewAccessibilityTimer];
2811  _activeView.accessibilityEnabled = NO;
2812  [_activeView resignFirstResponder];
2813  // Removes the focus from the `_activeView` (UIView<UITextInput>)
2814  // when the user stops typing (keyboard is hidden).
2815  // For more details, refer to the discussion at:
2816  // https://github.com/flutter/engine/pull/57209#discussion_r1905942577
2817  [self cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
2818 }
2819 
2820 - (void)triggerAutofillSave:(BOOL)saveEntries {
2821  [_activeView resignFirstResponder];
2822 
2823  if (saveEntries) {
2824  // Make all the input fields in the autofill context visible,
2825  // then remove them to trigger autofill save.
2826  [self cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
2827  [_autofillContext removeAllObjects];
2828  [self changeInputViewsAutofillVisibility:YES];
2829  } else {
2830  [_autofillContext removeAllObjects];
2831  }
2832 
2833  [self cleanUpViewHierarchy:YES clearText:!saveEntries delayRemoval:NO];
2834  [self addToInputParentViewIfNeeded:_activeView];
2835 }
2836 
2837 - (void)setPlatformViewTextInputClient {
2838  // No need to track the platformViewID (unlike in Android). When a platform view
2839  // becomes the first responder, simply hide this dummy text input view (`_activeView`)
2840  // for the previously focused widget.
2841  [self removeEnableFlutterTextInputViewAccessibilityTimer];
2842  _activeView.accessibilityEnabled = NO;
2843  [_activeView removeFromSuperview];
2844  [_inputHider removeFromSuperview];
2845 }
2846 
2847 - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configuration {
2848  [self resetAllClientIds];
2849  // Hide all input views from autofill, only make those in the new configuration visible
2850  // to autofill.
2851  [self changeInputViewsAutofillVisibility:NO];
2852 
2853  // Update the current active view.
2854  switch (AutofillTypeOf(configuration)) {
2855  case kFlutterAutofillTypeNone:
2856  self.activeView = [self createInputViewWith:configuration];
2857  break;
2858  case kFlutterAutofillTypeRegular:
2859  // If the group does not involve password autofill, only install the
2860  // input view that's being focused.
2861  self.activeView = [self updateAndShowAutofillViews:nil
2862  focusedField:configuration
2863  isPasswordRelated:NO];
2864  break;
2865  case kFlutterAutofillTypePassword:
2866  self.activeView = [self updateAndShowAutofillViews:configuration[kAssociatedAutofillFields]
2867  focusedField:configuration
2868  isPasswordRelated:YES];
2869  break;
2870  }
2871  [_activeView setTextInputClient:client];
2872  [_activeView reloadInputViews];
2873 
2874  // Clean up views that no longer need to be in the view hierarchy, according to
2875  // the current autofill context. The "garbage" input views are already made
2876  // invisible to autofill and they can't `becomeFirstResponder`, we only remove
2877  // them to free up resources and reduce the number of input views in the view
2878  // hierarchy.
2879  //
2880  // The garbage views are decommissioned immediately, but the removeFromSuperview
2881  // call is scheduled on the runloop and delayed by 0.1s so we don't remove the
2882  // text fields immediately (which seems to make the keyboard flicker).
2883  // See: https://github.com/flutter/flutter/issues/64628.
2884  [self cleanUpViewHierarchy:NO clearText:YES delayRemoval:YES];
2885 }
2886 
2887 // Creates and shows an input field that is not password related and has no autofill
2888 // info. This method returns a new FlutterTextInputView instance when called, since
2889 // UIKit uses the identity of `UITextInput` instances (or the identity of the input
2890 // views) to decide whether the IME's internal states should be reset. See:
2891 // https://github.com/flutter/flutter/issues/79031 .
2892 - (FlutterTextInputView*)createInputViewWith:(NSDictionary*)configuration {
2893  NSString* autofillId = AutofillIdFromDictionary(configuration);
2894  if (autofillId) {
2895  [_autofillContext removeObjectForKey:autofillId];
2896  }
2897  FlutterTextInputView* newView = [[FlutterTextInputView alloc] initWithOwner:self];
2898  [newView configureWithDictionary:configuration];
2899  [self addToInputParentViewIfNeeded:newView];
2900 
2901  for (NSDictionary* field in configuration[kAssociatedAutofillFields]) {
2902  NSString* autofillId = AutofillIdFromDictionary(field);
2903  if (autofillId && AutofillTypeOf(field) == kFlutterAutofillTypeNone) {
2904  [_autofillContext removeObjectForKey:autofillId];
2905  }
2906  }
2907  return newView;
2908 }
2909 
2910 - (FlutterTextInputView*)updateAndShowAutofillViews:(NSArray*)fields
2911  focusedField:(NSDictionary*)focusedField
2912  isPasswordRelated:(BOOL)isPassword {
2913  FlutterTextInputView* focused = nil;
2914  NSString* focusedId = AutofillIdFromDictionary(focusedField);
2915  NSAssert(focusedId, @"autofillId must not be null for the focused field: %@", focusedField);
2916 
2917  if (!fields) {
2918  // DO NOT push the current autofillable input fields to the context even
2919  // if it's password-related, because it is not in an autofill group.
2920  focused = [self getOrCreateAutofillableView:focusedField isPasswordAutofill:isPassword];
2921  [_autofillContext removeObjectForKey:focusedId];
2922  }
2923 
2924  for (NSDictionary* field in fields) {
2925  NSString* autofillId = AutofillIdFromDictionary(field);
2926  NSAssert(autofillId, @"autofillId must not be null for field: %@", field);
2927 
2928  BOOL hasHints = AutofillTypeOf(field) != kFlutterAutofillTypeNone;
2929  BOOL isFocused = [focusedId isEqualToString:autofillId];
2930 
2931  if (isFocused) {
2932  focused = [self getOrCreateAutofillableView:field isPasswordAutofill:isPassword];
2933  }
2934 
2935  if (hasHints) {
2936  // Push the current input field to the context if it has hints.
2937  _autofillContext[autofillId] = isFocused ? focused
2938  : [self getOrCreateAutofillableView:field
2939  isPasswordAutofill:isPassword];
2940  } else {
2941  // Mark for deletion.
2942  [_autofillContext removeObjectForKey:autofillId];
2943  }
2944  }
2945 
2946  NSAssert(focused, @"The current focused input view must not be nil.");
2947  return focused;
2948 }
2949 
2950 // Returns a new non-reusable input view (and put it into the view hierarchy), or get the
2951 // view from the current autofill context, if an input view with the same autofill id
2952 // already exists in the context.
2953 // This is generally used for input fields that are autofillable (UIKit tracks these veiws
2954 // for autofill purposes so they should not be reused for a different type of views).
2955 - (FlutterTextInputView*)getOrCreateAutofillableView:(NSDictionary*)field
2956  isPasswordAutofill:(BOOL)needsPasswordAutofill {
2957  NSString* autofillId = AutofillIdFromDictionary(field);
2958  FlutterTextInputView* inputView = _autofillContext[autofillId];
2959  if (!inputView) {
2960  inputView =
2961  needsPasswordAutofill ? [FlutterSecureTextInputView alloc] : [FlutterTextInputView alloc];
2962  inputView = [inputView initWithOwner:self];
2963  [self addToInputParentViewIfNeeded:inputView];
2964  }
2965 
2966  [inputView configureWithDictionary:field];
2967  return inputView;
2968 }
2969 
2970 // The UIView to add FlutterTextInputViews to.
2971 - (UIView*)hostView {
2972  UIView* host = _viewController.view;
2973  NSAssert(host != nullptr,
2974  @"The application must have a host view since the keyboard client "
2975  @"must be part of the responder chain to function. The host view controller is %@",
2976  _viewController);
2977  return host;
2978 }
2979 
2980 // The UIView to add FlutterTextInputViews to.
2981 - (NSArray<UIView*>*)textInputViews {
2982  return _inputHider.subviews;
2983 }
2984 
2985 // Removes every installed input field, unless it's in the current autofill context.
2986 //
2987 // The active view will be removed from its superview too, if includeActiveView is YES.
2988 // When clearText is YES, the text on the input fields will be set to empty before
2989 // they are removed from the view hierarchy, to avoid triggering autofill save.
2990 // If delayRemoval is true, removeFromSuperview will be scheduled on the runloop and
2991 // will be delayed by 0.1s so we don't remove the text fields immediately (which seems
2992 // to make the keyboard flicker).
2993 // See: https://github.com/flutter/flutter/issues/64628.
2994 
2995 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
2996  clearText:(BOOL)clearText
2997  delayRemoval:(BOOL)delayRemoval {
2998  for (UIView* view in self.textInputViews) {
2999  if ([view isKindOfClass:[FlutterTextInputView class]] &&
3000  (includeActiveView || view != _activeView)) {
3001  FlutterTextInputView* inputView = (FlutterTextInputView*)view;
3002  if (_autofillContext[inputView.autofillId] != view) {
3003  if (clearText) {
3004  [inputView replaceRangeLocal:NSMakeRange(0, inputView.text.length) withText:@""];
3005  }
3006  if (delayRemoval) {
3007  [inputView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:0.1];
3008  } else {
3009  [inputView removeFromSuperview];
3010  }
3011  }
3012  }
3013  }
3014 }
3015 
3016 // Changes the visibility of every FlutterTextInputView currently in the
3017 // view hierarchy.
3018 - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
3019  for (UIView* view in self.textInputViews) {
3020  if ([view isKindOfClass:[FlutterTextInputView class]]) {
3021  FlutterTextInputView* inputView = (FlutterTextInputView*)view;
3022  inputView.isVisibleToAutofill = newVisibility;
3023  }
3024  }
3025 }
3026 
3027 // Resets the client id of every FlutterTextInputView in the view hierarchy
3028 // to 0.
3029 // Called before establishing a new text input connection.
3030 // For views in the current autofill context, they need to
3031 // stay in the view hierachy but should not be allowed to
3032 // send messages (other than autofill related ones) to the
3033 // framework.
3034 - (void)resetAllClientIds {
3035  for (UIView* view in self.textInputViews) {
3036  if ([view isKindOfClass:[FlutterTextInputView class]]) {
3037  FlutterTextInputView* inputView = (FlutterTextInputView*)view;
3038  [inputView setTextInputClient:0];
3039  }
3040  }
3041 }
3042 
3043 - (void)addToInputParentViewIfNeeded:(FlutterTextInputView*)inputView {
3044  if (![inputView isDescendantOfView:_inputHider]) {
3045  [_inputHider addSubview:inputView];
3046  }
3047 
3048  if (_viewController.view == nil) {
3049  // If view controller's view has detached from flutter engine, we don't add _inputHider
3050  // in parent view to fallback and avoid crash.
3051  // https://github.com/flutter/flutter/issues/106404.
3052  return;
3053  }
3054  UIView* parentView = self.hostView;
3055  if (_inputHider.superview != parentView) {
3056  [parentView addSubview:_inputHider];
3057  }
3058 }
3059 
3060 - (void)setTextInputEditingState:(NSDictionary*)state {
3061  [_activeView setTextInputState:state];
3062 }
3063 
3064 - (void)clearTextInputClient {
3065  [_activeView setTextInputClient:0];
3066  _activeView.frame = CGRectZero;
3067 }
3068 
3069 - (void)updateConfig:(NSDictionary*)dictionary {
3070  BOOL isSecureTextEntry = [dictionary[kSecureTextEntry] boolValue];
3071  for (UIView* view in self.textInputViews) {
3072  if ([view isKindOfClass:[FlutterTextInputView class]]) {
3073  FlutterTextInputView* inputView = (FlutterTextInputView*)view;
3074  // The feature of holding and draging spacebar to move cursor is affected by
3075  // secureTextEntry, so when obscureText is updated, we need to update secureTextEntry
3076  // and call reloadInputViews.
3077  // https://github.com/flutter/flutter/issues/122139
3078  if (inputView.isSecureTextEntry != isSecureTextEntry) {
3079  inputView.secureTextEntry = isSecureTextEntry;
3080  [inputView reloadInputViews];
3081  }
3082  }
3083  }
3084 }
3085 
3086 #pragma mark UIIndirectScribbleInteractionDelegate
3087 
3088 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3089  isElementFocused:(UIScribbleElementIdentifier)elementIdentifier
3090  API_AVAILABLE(ios(14.0)) {
3091  return _activeView.scribbleFocusStatus == FlutterScribbleFocusStatusFocused;
3092 }
3093 
3094 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3095  focusElementIfNeeded:(UIScribbleElementIdentifier)elementIdentifier
3096  referencePoint:(CGPoint)focusReferencePoint
3097  completion:(void (^)(UIResponder<UITextInput>* focusedInput))completion
3098  API_AVAILABLE(ios(14.0)) {
3099  _activeView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
3100  [_indirectScribbleDelegate flutterTextInputPlugin:self
3101  focusElement:elementIdentifier
3102  atPoint:focusReferencePoint
3103  result:^(id _Nullable result) {
3104  _activeView.scribbleFocusStatus =
3105  FlutterScribbleFocusStatusFocused;
3106  completion(_activeView);
3107  }];
3108 }
3109 
3110 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3111  shouldDelayFocusForElement:(UIScribbleElementIdentifier)elementIdentifier
3112  API_AVAILABLE(ios(14.0)) {
3113  return NO;
3114 }
3115 
3116 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3117  willBeginWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
3118  API_AVAILABLE(ios(14.0)) {
3119 }
3120 
3121 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3122  didFinishWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
3123  API_AVAILABLE(ios(14.0)) {
3124 }
3125 
3126 - (CGRect)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3127  frameForElement:(UIScribbleElementIdentifier)elementIdentifier
3128  API_AVAILABLE(ios(14.0)) {
3129  NSValue* elementValue = [_scribbleElements objectForKey:elementIdentifier];
3130  if (elementValue == nil) {
3131  return CGRectZero;
3132  }
3133  return [elementValue CGRectValue];
3134 }
3135 
3136 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3137  requestElementsInRect:(CGRect)rect
3138  completion:
3139  (void (^)(NSArray<UIScribbleElementIdentifier>* elements))completion
3140  API_AVAILABLE(ios(14.0)) {
3141  [_indirectScribbleDelegate
3142  flutterTextInputPlugin:self
3143  requestElementsInRect:rect
3144  result:^(id _Nullable result) {
3145  NSMutableArray<UIScribbleElementIdentifier>* elements =
3146  [[NSMutableArray alloc] init];
3147  if ([result isKindOfClass:[NSArray class]]) {
3148  for (NSArray* elementArray in result) {
3149  [elements addObject:elementArray[0]];
3150  [_scribbleElements
3151  setObject:[NSValue
3152  valueWithCGRect:CGRectMake(
3153  [elementArray[1] floatValue],
3154  [elementArray[2] floatValue],
3155  [elementArray[3] floatValue],
3156  [elementArray[4] floatValue])]
3157  forKey:elementArray[0]];
3158  }
3159  }
3160  completion(elements);
3161  }];
3162 }
3163 
3164 #pragma mark - Methods related to Scribble support
3165 
3166 - (void)setUpIndirectScribbleInteraction:(id<FlutterViewResponder>)viewResponder {
3167  if (_viewResponder != viewResponder) {
3168  if (@available(iOS 14.0, *)) {
3169  UIView* parentView = viewResponder.view;
3170  if (parentView != nil) {
3171  UIIndirectScribbleInteraction* scribbleInteraction = [[UIIndirectScribbleInteraction alloc]
3172  initWithDelegate:(id<UIIndirectScribbleInteractionDelegate>)self];
3173  [parentView addInteraction:scribbleInteraction];
3174  }
3175  }
3176  }
3177  _viewResponder = viewResponder;
3178 }
3179 
3180 - (void)resetViewResponder {
3181  _viewResponder = nil;
3182 }
3183 
3184 #pragma mark -
3185 #pragma mark FlutterKeySecondaryResponder
3186 
3187 /**
3188  * Handles key down events received from the view controller, responding YES if
3189  * the event was handled.
3190  */
3191 - (BOOL)handlePress:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4)) {
3192  return NO;
3193 }
3194 @end
3195 
3196 /**
3197  * Recursively searches the UIView's subviews to locate the First Responder
3198  */
3199 @implementation UIView (FindFirstResponder)
3200 - (id)flutterFirstResponder {
3201  if (self.isFirstResponder) {
3202  return self;
3203  }
3204  for (UIView* subView in self.subviews) {
3205  UIView* firstResponder = subView.flutterFirstResponder;
3206  if (firstResponder) {
3207  return firstResponder;
3208  }
3209  }
3210  return nil;
3211 }
3212 @end
void(^ FlutterResult)(id _Nullable result)
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
NSRange _range
BOOL isScribbleAvailable
CGRect localRectFromFrameworkTransform
void resetScribbleInteractionStatusIfEnding
UITextRange * markedTextRange
id< UITextInputDelegate > inputDelegate
API_AVAILABLE(ios(13.0)) @interface FlutterTextPlaceholder UITextRange * selectedTextRange
instancetype initWithOwner
id< FlutterViewResponder > viewResponder
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
UIReturnKeyType returnKeyType
CGRect caretRectForPosition
UIKeyboardAppearance keyboardAppearance
static UITextAutocapitalizationType ToUITextAutoCapitalizationType(NSDictionary *type)
static NSString *const kSetMarkedTextRectMethod
static NSString *const kFinishAutofillContextMethod
CGRect _cachedFirstRect
bool _isFloatingCursorActive
static BOOL IsEmoji(NSString *text, NSRange charRange)
static NSString *const kAutofillHints
static NSString *const kAutofillEditingValue
FlutterTextRange * _selectedTextRange
static NSString *const kSecureTextEntry
bool _enableInteractiveSelection
static BOOL ShouldShowSystemKeyboard(NSDictionary *type)
static NSString *const kShowMethod
static NSString *const kUpdateConfigMethod
static UIKeyboardType ToUIKeyboardType(NSDictionary *type)
CGPoint _floatingCursorOffset
static NSString *const kSmartDashesType
static FLUTTER_ASSERT_ARC const char kTextAffinityDownstream[]
static constexpr double kUITextInputAccessibilityEnablingDelaySeconds
static NSString *const kSetSelectionRectsMethod
typedef NS_ENUM(NSInteger, FlutterAutofillType)
static NSString *const kAutocorrectionType
static NSString *const kSetPlatformViewClientMethod
static NSString *const kAssociatedAutofillFields
static NSString *const kKeyboardType
static const NSTimeInterval kKeyboardAnimationDelaySeconds
static NSString *const kSetEditingStateMethod
UIInputViewController * _inputViewController
static NSString *const kAutofillId
static NSString *const kOnInteractiveKeyboardPointerUpMethod
bool _isSystemKeyboardEnabled
static NSString *const kClearClientMethod
static NSString *const kKeyboardAppearance
const CGRect kInvalidFirstRect
static FlutterAutofillType AutofillTypeOf(NSDictionary *configuration)
static NSString *const kSmartQuotesType
static NSString *const kEnableDeltaModel
FlutterScribbleInteractionStatus _scribbleInteractionStatus
static NSString *const kSetClientMethod
static NSString *const kEnableInteractiveSelection
static NSString *const kInputAction
static NSString *const kStartLiveTextInputMethod
static NSString *const kAutofillProperties
static BOOL IsFieldPasswordRelated(NSDictionary *configuration)
static const NSTimeInterval kKeyboardAnimationTimeToCompleteion
static BOOL IsApproximatelyEqual(float x, float y, float delta)
static NSString *const kDeprecatedSetSelectionRectsMethod
static const char kTextAffinityUpstream[]
static NSString * AutofillIdFromDictionary(NSDictionary *dictionary)
static UIReturnKeyType ToUIReturnKeyType(NSString *inputType)
static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point, CGRect selectionRect, BOOL selectionRectIsRTL, BOOL useTrailingBoundaryOfSelectionRect, CGRect otherSelectionRect, BOOL otherSelectionRectIsRTL, CGFloat verticalPrecision)
static NSString *const kHideMethod
static UITextContentType ToUITextContentType(NSArray< NSString * > *hints)
static NSString *const kSetEditableSizeAndTransformMethod
static NSString *const kOnInteractiveKeyboardPointerMoveMethod
BOOL _hasPlaceholder
const char * _selectionAffinity
FlutterTextInputPlugin * textInputPlugin
instancetype positionWithIndex:(NSUInteger index)
instancetype positionWithIndex:affinity:(NSUInteger index,[affinity] UITextStorageDirection affinity)
UITextStorageDirection affinity
instancetype rangeWithNSRange:(NSRange range)
NSWritingDirection writingDirection
instancetype selectionRectWithRect:position:writingDirection:(CGRect rect,[position] NSUInteger position,[writingDirection] NSWritingDirection writingDirection)
instancetype selectionRectWithRectAndInfo:position:writingDirection:containsStart:containsEnd:isVertical:(CGRect rect,[position] NSUInteger position,[writingDirection] NSWritingDirection writingDirection,[containsStart] BOOL containsStart,[containsEnd] BOOL containsEnd,[isVertical] BOOL isVertical)
FlutterTextInputPlugin * target