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