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