Flutter iOS Embedder
FlutterTextInputPlugin.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
7 
8 #import <Foundation/Foundation.h>
9 #import <UIKit/UIKit.h>
10 
11 #include "unicode/uchar.h"
12 
13 #include "flutter/fml/logging.h"
14 #include "flutter/fml/platform/darwin/string_range_sanitization.h"
15 
17 
18 static const char kTextAffinityDownstream[] = "TextAffinity.downstream";
19 static const char kTextAffinityUpstream[] = "TextAffinity.upstream";
20 // A delay before enabling the accessibility of FlutterTextInputView after
21 // it is activated.
22 static constexpr double kUITextInputAccessibilityEnablingDelaySeconds = 0.5;
23 
24 // A delay before reenabling the UIView areAnimationsEnabled to YES
25 // in order for becomeFirstResponder to receive the proper value.
26 static const NSTimeInterval kKeyboardAnimationDelaySeconds = 0.1;
27 
28 // A time set for the screenshot to animate back to the assigned position.
29 static const NSTimeInterval kKeyboardAnimationTimeToCompleteion = 0.3;
30 
31 // The "canonical" invalid CGRect, similar to CGRectNull, used to
32 // indicate a CGRect involved in firstRectForRange calculation is
33 // invalid. The specific value is chosen so that if firstRectForRange
34 // returns kInvalidFirstRect, iOS will not show the IME candidates view.
35 const CGRect kInvalidFirstRect = {{-1, -1}, {9999, 9999}};
36 
37 #pragma mark - TextInput channel method names.
38 // See https://api.flutter.dev/flutter/services/SystemChannels/textInput-constant.html
39 static NSString* const kShowMethod = @"TextInput.show";
40 static NSString* const kHideMethod = @"TextInput.hide";
41 static NSString* const kSetClientMethod = @"TextInput.setClient";
42 static NSString* const kSetPlatformViewClientMethod = @"TextInput.setPlatformViewClient";
43 static NSString* const kSetEditingStateMethod = @"TextInput.setEditingState";
44 static NSString* const kClearClientMethod = @"TextInput.clearClient";
45 static NSString* const kSetEditableSizeAndTransformMethod =
46  @"TextInput.setEditableSizeAndTransform";
47 static NSString* const kSetMarkedTextRectMethod = @"TextInput.setMarkedTextRect";
48 static NSString* const kFinishAutofillContextMethod = @"TextInput.finishAutofillContext";
49 // TODO(justinmc): Remove the TextInput method constant when the framework has
50 // finished transitioning to using the Scribble channel.
51 // https://github.com/flutter/flutter/pull/104128
52 static NSString* const kDeprecatedSetSelectionRectsMethod = @"TextInput.setSelectionRects";
53 static NSString* const kSetSelectionRectsMethod = @"Scribble.setSelectionRects";
54 static NSString* const kStartLiveTextInputMethod = @"TextInput.startLiveTextInput";
55 static NSString* const kUpdateConfigMethod = @"TextInput.updateConfig";
57  @"TextInput.onPointerMoveForInteractiveKeyboard";
58 static NSString* const kOnInteractiveKeyboardPointerUpMethod =
59  @"TextInput.onPointerUpForInteractiveKeyboard";
60 
61 #pragma mark - TextInputConfiguration Field Names
62 static NSString* const kSecureTextEntry = @"obscureText";
63 static NSString* const kKeyboardType = @"inputType";
64 static NSString* const kKeyboardAppearance = @"keyboardAppearance";
65 static NSString* const kInputAction = @"inputAction";
66 static NSString* const kEnableDeltaModel = @"enableDeltaModel";
67 static NSString* const kEnableInteractiveSelection = @"enableInteractiveSelection";
68 
69 static NSString* const kSmartDashesType = @"smartDashesType";
70 static NSString* const kSmartQuotesType = @"smartQuotesType";
71 
72 static NSString* const kAssociatedAutofillFields = @"fields";
73 
74 // TextInputConfiguration.autofill and sub-field names
75 static NSString* const kAutofillProperties = @"autofill";
76 static NSString* const kAutofillId = @"uniqueIdentifier";
77 static NSString* const kAutofillEditingValue = @"editingValue";
78 static NSString* const kAutofillHints = @"hints";
79 
80 static NSString* const kAutocorrectionType = @"autocorrect";
81 
82 #pragma mark - Static Functions
83 
84 // Determine if the character at `range` of `text` is an emoji.
85 static BOOL IsEmoji(NSString* text, NSRange charRange) {
86  UChar32 codePoint;
87  BOOL gotCodePoint = [text getBytes:&codePoint
88  maxLength:sizeof(codePoint)
89  usedLength:NULL
90  encoding:NSUTF32StringEncoding
91  options:kNilOptions
92  range:charRange
93  remainingRange:NULL];
94  return gotCodePoint && u_hasBinaryProperty(codePoint, UCHAR_EMOJI);
95 }
96 
97 // "TextInputType.none" is a made-up input type that's typically
98 // used when there's an in-app virtual keyboard. If
99 // "TextInputType.none" is specified, disable the system
100 // keyboard.
101 static BOOL ShouldShowSystemKeyboard(NSDictionary* type) {
102  NSString* inputType = type[@"name"];
103  return ![inputType isEqualToString:@"TextInputType.none"];
104 }
105 static UIKeyboardType ToUIKeyboardType(NSDictionary* type) {
106  NSString* inputType = type[@"name"];
107  if ([inputType isEqualToString:@"TextInputType.address"]) {
108  return UIKeyboardTypeDefault;
109  }
110  if ([inputType isEqualToString:@"TextInputType.datetime"]) {
111  return UIKeyboardTypeNumbersAndPunctuation;
112  }
113  if ([inputType isEqualToString:@"TextInputType.emailAddress"]) {
114  return UIKeyboardTypeEmailAddress;
115  }
116  if ([inputType isEqualToString:@"TextInputType.multiline"]) {
117  return UIKeyboardTypeDefault;
118  }
119  if ([inputType isEqualToString:@"TextInputType.name"]) {
120  return UIKeyboardTypeNamePhonePad;
121  }
122  if ([inputType isEqualToString:@"TextInputType.number"]) {
123  if ([type[@"signed"] boolValue]) {
124  return UIKeyboardTypeNumbersAndPunctuation;
125  }
126  if ([type[@"decimal"] boolValue]) {
127  return UIKeyboardTypeDecimalPad;
128  }
129  return UIKeyboardTypeNumberPad;
130  }
131  if ([inputType isEqualToString:@"TextInputType.phone"]) {
132  return UIKeyboardTypePhonePad;
133  }
134  if ([inputType isEqualToString:@"TextInputType.text"]) {
135  return UIKeyboardTypeDefault;
136  }
137  if ([inputType isEqualToString:@"TextInputType.url"]) {
138  return UIKeyboardTypeURL;
139  }
140  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  }
1191  return [super canPerformAction:action withSender:sender];
1192 }
1193 
1194 #pragma mark - UIResponderStandardEditActions Overrides
1195 
1196 - (void)cut:(id)sender {
1197  [UIPasteboard generalPasteboard].string = [self textInRange:_selectedTextRange];
1198  [self replaceRange:_selectedTextRange withText:@""];
1199 }
1200 
1201 - (void)copy:(id)sender {
1202  [UIPasteboard generalPasteboard].string = [self textInRange:_selectedTextRange];
1203 }
1204 
1205 - (void)paste:(id)sender {
1206  NSString* pasteboardString = [UIPasteboard generalPasteboard].string;
1207  if (pasteboardString != nil) {
1208  [self insertText:pasteboardString];
1209  }
1210 }
1211 
1212 - (void)delete:(id)sender {
1213  [self replaceRange:_selectedTextRange withText:@""];
1214 }
1215 
1216 - (void)selectAll:(id)sender {
1217  [self setSelectedTextRange:[self textRangeFromPosition:[self beginningOfDocument]
1218  toPosition:[self endOfDocument]]];
1219 }
1220 
1221 #pragma mark - UITextInput Overrides
1222 
1223 - (id<UITextInputTokenizer>)tokenizer {
1224  if (_tokenizer == nil) {
1225  _tokenizer = [[FlutterTokenizer alloc] initWithTextInput:self];
1226  }
1227  return _tokenizer;
1228 }
1229 
1230 - (UITextRange*)selectedTextRange {
1231  return [_selectedTextRange copy];
1232 }
1233 
1234 // Change the range of selected text, without notifying the framework.
1235 - (void)setSelectedTextRangeLocal:(UITextRange*)selectedTextRange {
1237  if (self.hasText) {
1238  FlutterTextRange* flutterTextRange = (FlutterTextRange*)selectedTextRange;
1240  rangeWithNSRange:fml::RangeForCharactersInRange(self.text, flutterTextRange.range)] copy];
1241  } else {
1242  _selectedTextRange = [selectedTextRange copy];
1243  }
1244  }
1245 }
1246 
1247 - (void)setSelectedTextRange:(UITextRange*)selectedTextRange {
1249  return;
1250  }
1251 
1252  [self setSelectedTextRangeLocal:selectedTextRange];
1253 
1254  if (_enableDeltaModel) {
1255  [self updateEditingStateWithDelta:flutter::TextEditingDelta([self.text UTF8String])];
1256  } else {
1257  [self updateEditingState];
1258  }
1259 
1260  if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone ||
1261  _scribbleFocusStatus == FlutterScribbleFocusStatusFocused) {
1262  NSAssert([selectedTextRange isKindOfClass:[FlutterTextRange class]],
1263  @"Expected a FlutterTextRange for range (got %@).", [selectedTextRange class]);
1264  FlutterTextRange* flutterTextRange = (FlutterTextRange*)selectedTextRange;
1265  if (flutterTextRange.range.length > 0) {
1266  [self.textInputDelegate flutterTextInputView:self showToolbar:_textInputClient];
1267  }
1268  }
1269 
1270  [self resetScribbleInteractionStatusIfEnding];
1271 }
1272 
1273 - (id)insertDictationResultPlaceholder {
1274  return @"";
1275 }
1276 
1277 - (void)removeDictationResultPlaceholder:(id)placeholder willInsertResult:(BOOL)willInsertResult {
1278 }
1279 
1280 - (NSString*)textInRange:(UITextRange*)range {
1281  if (!range) {
1282  return nil;
1283  }
1284  NSAssert([range isKindOfClass:[FlutterTextRange class]],
1285  @"Expected a FlutterTextRange for range (got %@).", [range class]);
1286  NSRange textRange = ((FlutterTextRange*)range).range;
1287  NSAssert(textRange.location != NSNotFound, @"Expected a valid text range.");
1288  // Sanitize the range to prevent going out of bounds.
1289  NSUInteger location = MIN(textRange.location, self.text.length);
1290  NSUInteger length = MIN(self.text.length - location, textRange.length);
1291  NSRange safeRange = NSMakeRange(location, length);
1292  return [self.text substringWithRange:safeRange];
1293 }
1294 
1295 // Replace the text within the specified range with the given text,
1296 // without notifying the framework.
1297 - (void)replaceRangeLocal:(NSRange)range withText:(NSString*)text {
1298  [self.text replaceCharactersInRange:[self clampSelection:range forText:self.text]
1299  withString:text];
1300 
1301  // Adjust the selected range and the marked text range. There's no
1302  // documentation but UITextField always sets markedTextRange to nil,
1303  // and collapses the selection to the end of the new replacement text.
1304  const NSRange newSelectionRange =
1305  [self clampSelection:NSMakeRange(range.location + text.length, 0) forText:self.text];
1306 
1307  [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:newSelectionRange]];
1308  self.markedTextRange = nil;
1309 }
1310 
1311 - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
1312  NSString* textBeforeChange = [self.text copy];
1313  NSRange replaceRange = ((FlutterTextRange*)range).range;
1314  [self replaceRangeLocal:replaceRange withText:text];
1315  if (_enableDeltaModel) {
1316  NSRange nextReplaceRange = [self clampSelection:replaceRange forText:textBeforeChange];
1317  [self updateEditingStateWithDelta:flutter::TextEditingDelta(
1318  [textBeforeChange UTF8String],
1319  flutter::TextRange(
1320  nextReplaceRange.location,
1321  nextReplaceRange.location + nextReplaceRange.length),
1322  [text UTF8String])];
1323  } else {
1324  [self updateEditingState];
1325  }
1326 }
1327 
1328 - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
1329  // `temporarilyDeletedComposedCharacter` should only be used during a single text change session.
1330  // So it needs to be cleared at the start of each text editing session.
1331  self.temporarilyDeletedComposedCharacter = nil;
1332 
1333  if (self.returnKeyType == UIReturnKeyDefault && [text isEqualToString:@"\n"]) {
1334  [self.textInputDelegate flutterTextInputView:self
1335  performAction:FlutterTextInputActionNewline
1336  withClient:_textInputClient];
1337  return YES;
1338  }
1339 
1340  if ([text isEqualToString:@"\n"]) {
1341  FlutterTextInputAction action;
1342  switch (self.returnKeyType) {
1343  case UIReturnKeyDefault:
1344  action = FlutterTextInputActionUnspecified;
1345  break;
1346  case UIReturnKeyDone:
1347  action = FlutterTextInputActionDone;
1348  break;
1349  case UIReturnKeyGo:
1350  action = FlutterTextInputActionGo;
1351  break;
1352  case UIReturnKeySend:
1353  action = FlutterTextInputActionSend;
1354  break;
1355  case UIReturnKeySearch:
1356  case UIReturnKeyGoogle:
1357  case UIReturnKeyYahoo:
1358  action = FlutterTextInputActionSearch;
1359  break;
1360  case UIReturnKeyNext:
1361  action = FlutterTextInputActionNext;
1362  break;
1363  case UIReturnKeyContinue:
1364  action = FlutterTextInputActionContinue;
1365  break;
1366  case UIReturnKeyJoin:
1367  action = FlutterTextInputActionJoin;
1368  break;
1369  case UIReturnKeyRoute:
1370  action = FlutterTextInputActionRoute;
1371  break;
1372  case UIReturnKeyEmergencyCall:
1373  action = FlutterTextInputActionEmergencyCall;
1374  break;
1375  }
1376 
1377  [self.textInputDelegate flutterTextInputView:self
1378  performAction:action
1379  withClient:_textInputClient];
1380  return NO;
1381  }
1382 
1383  return YES;
1384 }
1385 
1386 // Either replaces the existing marked text or, if none is present, inserts it in
1387 // place of the current selection.
1388 - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelectedRange {
1389  NSString* textBeforeChange = [self.text copy];
1390 
1391  if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone ||
1392  _scribbleFocusStatus != FlutterScribbleFocusStatusUnfocused) {
1393  return;
1394  }
1395 
1396  if (markedText == nil) {
1397  markedText = @"";
1398  }
1399 
1400  const FlutterTextRange* currentMarkedTextRange = (FlutterTextRange*)self.markedTextRange;
1401  const NSRange& actualReplacedRange = currentMarkedTextRange && !currentMarkedTextRange.isEmpty
1402  ? currentMarkedTextRange.range
1404  // No need to call replaceRangeLocal as this method always adjusts the
1405  // selected/marked text ranges anyways.
1406  [self.text replaceCharactersInRange:actualReplacedRange withString:markedText];
1407 
1408  const NSRange newMarkedRange = NSMakeRange(actualReplacedRange.location, markedText.length);
1409  self.markedTextRange =
1410  newMarkedRange.length > 0 ? [FlutterTextRange rangeWithNSRange:newMarkedRange] : nil;
1411 
1412  [self setSelectedTextRangeLocal:
1414  rangeWithNSRange:[self clampSelection:NSMakeRange(markedSelectedRange.location +
1415  newMarkedRange.location,
1416  markedSelectedRange.length)
1417  forText:self.text]]];
1418  if (_enableDeltaModel) {
1419  NSRange nextReplaceRange = [self clampSelection:actualReplacedRange forText:textBeforeChange];
1420  [self updateEditingStateWithDelta:flutter::TextEditingDelta(
1421  [textBeforeChange UTF8String],
1422  flutter::TextRange(
1423  nextReplaceRange.location,
1424  nextReplaceRange.location + nextReplaceRange.length),
1425  [markedText UTF8String])];
1426  } else {
1427  [self updateEditingState];
1428  }
1429 }
1430 
1431 - (void)unmarkText {
1432  if (!self.markedTextRange) {
1433  return;
1434  }
1435  self.markedTextRange = nil;
1436  if (_enableDeltaModel) {
1437  [self updateEditingStateWithDelta:flutter::TextEditingDelta([self.text UTF8String])];
1438  } else {
1439  [self updateEditingState];
1440  }
1441 }
1442 
1443 - (UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition
1444  toPosition:(UITextPosition*)toPosition {
1445  NSUInteger fromIndex = ((FlutterTextPosition*)fromPosition).index;
1446  NSUInteger toIndex = ((FlutterTextPosition*)toPosition).index;
1447  if (toIndex >= fromIndex) {
1448  return [FlutterTextRange rangeWithNSRange:NSMakeRange(fromIndex, toIndex - fromIndex)];
1449  } else {
1450  // toIndex can be smaller than fromIndex, because
1451  // UITextInputStringTokenizer does not handle CJK characters
1452  // well in some cases. See:
1453  // https://github.com/flutter/flutter/issues/58750#issuecomment-644469521
1454  // Swap fromPosition and toPosition to match the behavior of native
1455  // UITextViews.
1456  return [FlutterTextRange rangeWithNSRange:NSMakeRange(toIndex, fromIndex - toIndex)];
1457  }
1458 }
1459 
1460 - (NSUInteger)decrementOffsetPosition:(NSUInteger)position {
1461  return fml::RangeForCharacterAtIndex(self.text, MAX(0, position - 1)).location;
1462 }
1463 
1464 - (NSUInteger)incrementOffsetPosition:(NSUInteger)position {
1465  NSRange charRange = fml::RangeForCharacterAtIndex(self.text, position);
1466  return MIN(position + charRange.length, self.text.length);
1467 }
1468 
1469 - (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInteger)offset {
1470  NSUInteger offsetPosition = ((FlutterTextPosition*)position).index;
1471 
1472  NSInteger newLocation = (NSInteger)offsetPosition + offset;
1473  if (newLocation < 0 || newLocation > (NSInteger)self.text.length) {
1474  return nil;
1475  }
1476 
1477  if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone) {
1478  return [FlutterTextPosition positionWithIndex:newLocation];
1479  }
1480 
1481  if (offset >= 0) {
1482  for (NSInteger i = 0; i < offset && offsetPosition < self.text.length; ++i) {
1483  offsetPosition = [self incrementOffsetPosition:offsetPosition];
1484  }
1485  } else {
1486  for (NSInteger i = 0; i < ABS(offset) && offsetPosition > 0; ++i) {
1487  offsetPosition = [self decrementOffsetPosition:offsetPosition];
1488  }
1489  }
1490  return [FlutterTextPosition positionWithIndex:offsetPosition];
1491 }
1492 
1493 - (UITextPosition*)positionFromPosition:(UITextPosition*)position
1494  inDirection:(UITextLayoutDirection)direction
1495  offset:(NSInteger)offset {
1496  // TODO(cbracken) Add RTL handling.
1497  switch (direction) {
1498  case UITextLayoutDirectionLeft:
1499  case UITextLayoutDirectionUp:
1500  return [self positionFromPosition:position offset:offset * -1];
1501  case UITextLayoutDirectionRight:
1502  case UITextLayoutDirectionDown:
1503  return [self positionFromPosition:position offset:1];
1504  }
1505 }
1506 
1507 - (UITextPosition*)beginningOfDocument {
1508  return [FlutterTextPosition positionWithIndex:0 affinity:UITextStorageDirectionForward];
1509 }
1510 
1511 - (UITextPosition*)endOfDocument {
1512  return [FlutterTextPosition positionWithIndex:self.text.length
1513  affinity:UITextStorageDirectionBackward];
1514 }
1515 
1516 - (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other {
1517  NSUInteger positionIndex = ((FlutterTextPosition*)position).index;
1518  NSUInteger otherIndex = ((FlutterTextPosition*)other).index;
1519  if (positionIndex < otherIndex) {
1520  return NSOrderedAscending;
1521  }
1522  if (positionIndex > otherIndex) {
1523  return NSOrderedDescending;
1524  }
1525  UITextStorageDirection positionAffinity = ((FlutterTextPosition*)position).affinity;
1526  UITextStorageDirection otherAffinity = ((FlutterTextPosition*)other).affinity;
1527  if (positionAffinity == otherAffinity) {
1528  return NSOrderedSame;
1529  }
1530  if (positionAffinity == UITextStorageDirectionBackward) {
1531  // positionAffinity points backwards, otherAffinity points forwards
1532  return NSOrderedAscending;
1533  }
1534  // positionAffinity points forwards, otherAffinity points backwards
1535  return NSOrderedDescending;
1536 }
1537 
1538 - (NSInteger)offsetFromPosition:(UITextPosition*)from toPosition:(UITextPosition*)toPosition {
1539  return ((FlutterTextPosition*)toPosition).index - ((FlutterTextPosition*)from).index;
1540 }
1541 
1542 - (UITextPosition*)positionWithinRange:(UITextRange*)range
1543  farthestInDirection:(UITextLayoutDirection)direction {
1544  NSUInteger index;
1545  UITextStorageDirection affinity;
1546  switch (direction) {
1547  case UITextLayoutDirectionLeft:
1548  case UITextLayoutDirectionUp:
1549  index = ((FlutterTextPosition*)range.start).index;
1550  affinity = UITextStorageDirectionForward;
1551  break;
1552  case UITextLayoutDirectionRight:
1553  case UITextLayoutDirectionDown:
1554  index = ((FlutterTextPosition*)range.end).index;
1555  affinity = UITextStorageDirectionBackward;
1556  break;
1557  }
1558  return [FlutterTextPosition positionWithIndex:index affinity:affinity];
1559 }
1560 
1561 - (UITextRange*)characterRangeByExtendingPosition:(UITextPosition*)position
1562  inDirection:(UITextLayoutDirection)direction {
1563  NSUInteger positionIndex = ((FlutterTextPosition*)position).index;
1564  NSUInteger startIndex;
1565  NSUInteger endIndex;
1566  switch (direction) {
1567  case UITextLayoutDirectionLeft:
1568  case UITextLayoutDirectionUp:
1569  startIndex = [self decrementOffsetPosition:positionIndex];
1570  endIndex = positionIndex;
1571  break;
1572  case UITextLayoutDirectionRight:
1573  case UITextLayoutDirectionDown:
1574  startIndex = positionIndex;
1575  endIndex = [self incrementOffsetPosition:positionIndex];
1576  break;
1577  }
1578  return [FlutterTextRange rangeWithNSRange:NSMakeRange(startIndex, endIndex - startIndex)];
1579 }
1580 
1581 #pragma mark - UITextInput text direction handling
1582 
1583 - (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position
1584  inDirection:(UITextStorageDirection)direction {
1585  // TODO(cbracken) Add RTL handling.
1586  return UITextWritingDirectionNatural;
1587 }
1588 
1589 - (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection
1590  forRange:(UITextRange*)range {
1591  // TODO(cbracken) Add RTL handling.
1592 }
1593 
1594 #pragma mark - UITextInput cursor, selection rect handling
1595 
1596 - (void)setMarkedRect:(CGRect)markedRect {
1597  _markedRect = markedRect;
1598  // Invalidate the cache.
1600 }
1601 
1602 // This method expects a 4x4 perspective matrix
1603 // stored in a NSArray in column-major order.
1604 - (void)setEditableTransform:(NSArray*)matrix {
1605  CATransform3D* transform = &_editableTransform;
1606 
1607  transform->m11 = [matrix[0] doubleValue];
1608  transform->m12 = [matrix[1] doubleValue];
1609  transform->m13 = [matrix[2] doubleValue];
1610  transform->m14 = [matrix[3] doubleValue];
1611 
1612  transform->m21 = [matrix[4] doubleValue];
1613  transform->m22 = [matrix[5] doubleValue];
1614  transform->m23 = [matrix[6] doubleValue];
1615  transform->m24 = [matrix[7] doubleValue];
1616 
1617  transform->m31 = [matrix[8] doubleValue];
1618  transform->m32 = [matrix[9] doubleValue];
1619  transform->m33 = [matrix[10] doubleValue];
1620  transform->m34 = [matrix[11] doubleValue];
1621 
1622  transform->m41 = [matrix[12] doubleValue];
1623  transform->m42 = [matrix[13] doubleValue];
1624  transform->m43 = [matrix[14] doubleValue];
1625  transform->m44 = [matrix[15] doubleValue];
1626 
1627  // Invalidate the cache.
1629 }
1630 
1631 // Returns the bounding CGRect of the transformed incomingRect, in the view's
1632 // coordinates.
1633 - (CGRect)localRectFromFrameworkTransform:(CGRect)incomingRect {
1634  CGPoint points[] = {
1635  incomingRect.origin,
1636  CGPointMake(incomingRect.origin.x, incomingRect.origin.y + incomingRect.size.height),
1637  CGPointMake(incomingRect.origin.x + incomingRect.size.width, incomingRect.origin.y),
1638  CGPointMake(incomingRect.origin.x + incomingRect.size.width,
1639  incomingRect.origin.y + incomingRect.size.height)};
1640 
1641  CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
1642  CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
1643 
1644  for (int i = 0; i < 4; i++) {
1645  const CGPoint point = points[i];
1646 
1647  CGFloat x = _editableTransform.m11 * point.x + _editableTransform.m21 * point.y +
1648  _editableTransform.m41;
1649  CGFloat y = _editableTransform.m12 * point.x + _editableTransform.m22 * point.y +
1650  _editableTransform.m42;
1651 
1652  const CGFloat w = _editableTransform.m14 * point.x + _editableTransform.m24 * point.y +
1653  _editableTransform.m44;
1654 
1655  if (w == 0.0) {
1656  return kInvalidFirstRect;
1657  } else if (w != 1.0) {
1658  x /= w;
1659  y /= w;
1660  }
1661 
1662  origin.x = MIN(origin.x, x);
1663  origin.y = MIN(origin.y, y);
1664  farthest.x = MAX(farthest.x, x);
1665  farthest.y = MAX(farthest.y, y);
1666  }
1667  return CGRectMake(origin.x, origin.y, farthest.x - origin.x, farthest.y - origin.y);
1668 }
1669 
1670 // The following methods are required to support force-touch cursor positioning
1671 // and to position the
1672 // candidates view for multi-stage input methods (e.g., Japanese) when using a
1673 // physical keyboard.
1674 // Returns the rect for the queried range, or a subrange through the end of line, if
1675 // the range encompasses multiple lines.
1676 - (CGRect)firstRectForRange:(UITextRange*)range {
1677  NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
1678  @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
1679  NSAssert([range.end isKindOfClass:[FlutterTextPosition class]],
1680  @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]);
1681  NSUInteger start = ((FlutterTextPosition*)range.start).index;
1682  NSUInteger end = ((FlutterTextPosition*)range.end).index;
1683  if (_markedTextRange != nil) {
1684  // The candidates view can't be shown if the framework has not sent the
1685  // first caret rect.
1686  if (CGRectEqualToRect(kInvalidFirstRect, _markedRect)) {
1687  return kInvalidFirstRect;
1688  }
1689 
1690  if (CGRectEqualToRect(_cachedFirstRect, kInvalidFirstRect)) {
1691  // If the width returned is too small, that means the framework sent us
1692  // the caret rect instead of the marked text rect. Expand it to 0.2 so
1693  // the IME candidates view would show up.
1694  CGRect rect = _markedRect;
1695  if (CGRectIsEmpty(rect)) {
1696  rect = CGRectInset(rect, -0.1, 0);
1697  }
1698  _cachedFirstRect = [self localRectFromFrameworkTransform:rect];
1699  }
1700 
1701  UIView* hostView = _textInputPlugin.hostView;
1702  NSAssert(hostView == nil || [self isDescendantOfView:hostView], @"%@ is not a descendant of %@",
1703  self, hostView);
1704  return hostView ? [hostView convertRect:_cachedFirstRect toView:self] : _cachedFirstRect;
1705  }
1706 
1707  if (_scribbleInteractionStatus == FlutterScribbleInteractionStatusNone &&
1708  _scribbleFocusStatus == FlutterScribbleFocusStatusUnfocused) {
1709  if (@available(iOS 17.0, *)) {
1710  // Disable auto-correction highlight feature for iOS 17+.
1711  // In iOS 17+, whenever a character is inserted or deleted, the system will always query
1712  // the rect for every single character of the current word.
1713  // GitHub Issue: https://github.com/flutter/flutter/issues/128406
1714  } else {
1715  // This tells the framework to show the highlight for incorrectly spelled word that is
1716  // about to be auto-corrected.
1717  // There is no other UITextInput API that informs about the auto-correction highlight.
1718  // So we simply add the call here as a workaround.
1719  [self.textInputDelegate flutterTextInputView:self
1720  showAutocorrectionPromptRectForStart:start
1721  end:end
1722  withClient:_textInputClient];
1723  }
1724  }
1725 
1726  // The iOS 16 system highlight does not repect the height returned by `firstRectForRange`
1727  // API (unlike iOS 17). So we return CGRectZero to hide it (unless if scribble is enabled).
1728  // To support scribble's advanced gestures (e.g. insert a space with a vertical bar),
1729  // at least 1 character's width is required.
1730  if (@available(iOS 17, *)) {
1731  // No-op
1732  } else if (![self isScribbleAvailable]) {
1733  return CGRectZero;
1734  }
1735 
1736  NSUInteger first = start;
1737  if (end < start) {
1738  first = end;
1739  }
1740 
1741  CGRect startSelectionRect = CGRectNull;
1742  CGRect endSelectionRect = CGRectNull;
1743  // Selection rects from different langauges may have different minY/maxY.
1744  // So we need to iterate through each rects to update minY/maxY.
1745  CGFloat minY = CGFLOAT_MAX;
1746  CGFloat maxY = CGFLOAT_MIN;
1747 
1748  FlutterTextRange* textRange = [FlutterTextRange
1749  rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1750  for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1751  BOOL startsOnOrBeforeStartOfRange = _selectionRects[i].position <= first;
1752  BOOL isLastSelectionRect = i + 1 == [_selectionRects count];
1753  BOOL endOfTextIsAfterStartOfRange = isLastSelectionRect && textRange.range.length > first;
1754  BOOL nextSelectionRectIsAfterStartOfRange =
1755  !isLastSelectionRect && _selectionRects[i + 1].position > first;
1756  if (startsOnOrBeforeStartOfRange &&
1757  (endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
1758  // TODO(hellohaunlin): Remove iOS 17 check. The logic should also work for older versions.
1759  if (@available(iOS 17, *)) {
1760  startSelectionRect = _selectionRects[i].rect;
1761  } else {
1762  return _selectionRects[i].rect;
1763  }
1764  }
1765  if (!CGRectIsNull(startSelectionRect)) {
1766  minY = fmin(minY, CGRectGetMinY(_selectionRects[i].rect));
1767  maxY = fmax(maxY, CGRectGetMaxY(_selectionRects[i].rect));
1768  BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1; // end is exclusive
1769  BOOL nextSelectionRectIsOnNextLine =
1770  !isLastSelectionRect &&
1771  // Selection rects from different langauges in 2 lines may overlap with each other.
1772  // A good approximation is to check if the center of next rect is below the bottom of
1773  // current rect.
1774  // TODO(hellohuanlin): Consider passing the line break info from framework.
1775  CGRectGetMidY(_selectionRects[i + 1].rect) > CGRectGetMaxY(_selectionRects[i].rect);
1776  if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectionRectIsOnNextLine) {
1777  endSelectionRect = _selectionRects[i].rect;
1778  break;
1779  }
1780  }
1781  }
1782  if (CGRectIsNull(startSelectionRect) || CGRectIsNull(endSelectionRect)) {
1783  return CGRectZero;
1784  } else {
1785  // fmin/fmax to support both LTR and RTL languages.
1786  CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
1787  CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
1788  return CGRectMake(minX, minY, maxX - minX, maxY - minY);
1789  }
1790 }
1791 
1792 - (CGRect)caretRectForPosition:(UITextPosition*)position {
1793  NSInteger index = ((FlutterTextPosition*)position).index;
1794  UITextStorageDirection affinity = ((FlutterTextPosition*)position).affinity;
1795  // Get the selectionRect of the characters before and after the requested caret position.
1796  NSArray<UITextSelectionRect*>* rects = [self
1797  selectionRectsForRange:[FlutterTextRange
1798  rangeWithNSRange:fml::RangeForCharactersInRange(
1799  self.text,
1800  NSMakeRange(
1801  MAX(0, index - 1),
1802  (index >= (NSInteger)self.text.length)
1803  ? 1
1804  : 2))]];
1805  if (rects.count == 0) {
1806  return CGRectZero;
1807  }
1808  if (index == 0) {
1809  // There is no character before the caret, so this will be the bounds of the character after the
1810  // caret position.
1811  CGRect characterAfterCaret = rects[0].rect;
1812  // Return a zero-width rectangle along the upstream edge of the character after the caret
1813  // position.
1814  if ([rects[0] isKindOfClass:[FlutterTextSelectionRect class]] &&
1815  ((FlutterTextSelectionRect*)rects[0]).isRTL) {
1816  return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1817  characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1818  } else {
1819  return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1820  characterAfterCaret.size.height);
1821  }
1822  } else if (rects.count == 2 && affinity == UITextStorageDirectionForward) {
1823  // There are characters before and after the caret, with forward direction affinity.
1824  // It's better to use the character after the caret.
1825  CGRect characterAfterCaret = rects[1].rect;
1826  // Return a zero-width rectangle along the upstream edge of the character after the caret
1827  // position.
1828  if ([rects[1] isKindOfClass:[FlutterTextSelectionRect class]] &&
1829  ((FlutterTextSelectionRect*)rects[1]).isRTL) {
1830  return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1831  characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1832  } else {
1833  return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1834  characterAfterCaret.size.height);
1835  }
1836  }
1837 
1838  // Covers 2 remaining cases:
1839  // 1. there are characters before and after the caret, with backward direction affinity.
1840  // 2. there is only 1 character before the caret (caret is at the end of text).
1841  // For both cases, return a zero-width rectangle along the downstream edge of the character
1842  // before the caret position.
1843  CGRect characterBeforeCaret = rects[0].rect;
1844  if ([rects[0] isKindOfClass:[FlutterTextSelectionRect class]] &&
1845  ((FlutterTextSelectionRect*)rects[0]).isRTL) {
1846  return CGRectMake(characterBeforeCaret.origin.x, characterBeforeCaret.origin.y, 0,
1847  characterBeforeCaret.size.height);
1848  } else {
1849  return CGRectMake(characterBeforeCaret.origin.x + characterBeforeCaret.size.width,
1850  characterBeforeCaret.origin.y, 0, characterBeforeCaret.size.height);
1851  }
1852 }
1853 
1854 - (UITextPosition*)closestPositionToPoint:(CGPoint)point {
1855  if ([_selectionRects count] == 0) {
1856  NSAssert([_selectedTextRange.start isKindOfClass:[FlutterTextPosition class]],
1857  @"Expected a FlutterTextPosition for position (got %@).",
1858  [_selectedTextRange.start class]);
1859  NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index;
1860  UITextStorageDirection currentAffinity =
1861  ((FlutterTextPosition*)_selectedTextRange.start).affinity;
1862  return [FlutterTextPosition positionWithIndex:currentIndex affinity:currentAffinity];
1863  }
1864 
1866  rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1867  return [self closestPositionToPoint:point withinRange:range];
1868 }
1869 
1870 - (NSArray*)selectionRectsForRange:(UITextRange*)range {
1871  // At least in the simulator, swapping to the Japanese keyboard crashes the app as this method
1872  // is called immediately with a UITextRange with a UITextPosition rather than FlutterTextPosition
1873  // for the start and end.
1874  if (![range.start isKindOfClass:[FlutterTextPosition class]]) {
1875  return @[];
1876  }
1877  NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
1878  @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
1879  NSAssert([range.end isKindOfClass:[FlutterTextPosition class]],
1880  @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]);
1881  NSUInteger start = ((FlutterTextPosition*)range.start).index;
1882  NSUInteger end = ((FlutterTextPosition*)range.end).index;
1883  NSMutableArray* rects = [[NSMutableArray alloc] init];
1884  for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1885  if (_selectionRects[i].position >= start &&
1886  (_selectionRects[i].position < end ||
1887  (start == end && _selectionRects[i].position <= end))) {
1888  float width = _selectionRects[i].rect.size.width;
1889  if (start == end) {
1890  width = 0;
1891  }
1892  CGRect rect = CGRectMake(_selectionRects[i].rect.origin.x, _selectionRects[i].rect.origin.y,
1893  width, _selectionRects[i].rect.size.height);
1896  position:_selectionRects[i].position
1897  writingDirection:NSWritingDirectionNatural
1898  containsStart:(i == 0)
1899  containsEnd:(i == fml::RangeForCharactersInRange(
1900  self.text, NSMakeRange(0, self.text.length))
1901  .length)
1902  isVertical:NO];
1903  [rects addObject:selectionRect];
1904  }
1905  }
1906  return rects;
1907 }
1908 
1909 - (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range {
1910  NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
1911  @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
1912  NSAssert([range.end isKindOfClass:[FlutterTextPosition class]],
1913  @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]);
1914  NSUInteger start = ((FlutterTextPosition*)range.start).index;
1915  NSUInteger end = ((FlutterTextPosition*)range.end).index;
1916 
1917  // Selecting text using the floating cursor is not as precise as the pencil.
1918  // Allow further vertical deviation and base more of the decision on horizontal comparison.
1919  CGFloat verticalPrecision = _isFloatingCursorActive ? 10 : 1;
1920 
1921  // Find the selectionRect with a leading-center point that is closest to a given point.
1922  BOOL isFirst = YES;
1923  NSUInteger _closestRectIndex = 0;
1924  for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1925  NSUInteger position = _selectionRects[i].position;
1926  if (position >= start && position <= end) {
1927  if (isFirst ||
1929  point, _selectionRects[i].rect, _selectionRects[i].isRTL,
1930  /*useTrailingBoundaryOfSelectionRect=*/NO, _selectionRects[_closestRectIndex].rect,
1931  _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
1932  isFirst = NO;
1933  _closestRectIndex = i;
1934  }
1935  }
1936  }
1937 
1938  FlutterTextPosition* closestPosition =
1939  [FlutterTextPosition positionWithIndex:_selectionRects[_closestRectIndex].position
1940  affinity:UITextStorageDirectionForward];
1941 
1942  // Check if the far side of the closest rect is a better fit (e.g. tapping end of line)
1943  // Cannot simply check the _closestRectIndex result from the previous for loop due to RTL
1944  // writing direction and the gaps between selectionRects. So we also need to consider
1945  // the adjacent selectionRects to refine _closestRectIndex.
1946  for (NSUInteger i = MAX(0, _closestRectIndex - 1);
1947  i < MIN(_closestRectIndex + 2, [_selectionRects count]); i++) {
1948  NSUInteger position = _selectionRects[i].position + 1;
1949  if (position >= start && position <= end) {
1951  point, _selectionRects[i].rect, _selectionRects[i].isRTL,
1952  /*useTrailingBoundaryOfSelectionRect=*/YES, _selectionRects[_closestRectIndex].rect,
1953  _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
1954  // This is an upstream position
1955  closestPosition = [FlutterTextPosition positionWithIndex:position
1956  affinity:UITextStorageDirectionBackward];
1957  }
1958  }
1959  }
1960 
1961  return closestPosition;
1962 }
1963 
1964 - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
1965  // TODO(cbracken) Implement.
1966  NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index;
1967  return [FlutterTextRange rangeWithNSRange:fml::RangeForCharacterAtIndex(self.text, currentIndex)];
1968 }
1969 
1970 // Overall logic for floating cursor's "move" gesture and "selection" gesture:
1971 //
1972 // Floating cursor's "move" gesture takes 1 finger to force press the space bar, and then move the
1973 // cursor. The process starts with `beginFloatingCursorAtPoint`. When the finger is moved,
1974 // `updateFloatingCursorAtPoint` will be called. When the finger is released, `endFloatingCursor`
1975 // will be called. In all cases, we send the point (relative to the initial point registered in
1976 // beginFloatingCursorAtPoint) to the framework, so that framework can animate the floating cursor.
1977 //
1978 // During the move gesture, the framework only animate the cursor visually. It's only
1979 // after the gesture is complete, will the framework update the selection to the cursor's
1980 // new position (with zero selection length). This means during the animation, the visual effect
1981 // of the cursor is temporarily out of sync with the selection state in both framework and engine.
1982 // But it will be in sync again after the animation is complete.
1983 //
1984 // Floating cursor's "selection" gesture also starts with 1 finger to force press the space bar,
1985 // so exactly the same functions as the "move gesture" discussed above will be called. When the
1986 // second finger is pressed, `setSelectedText` will be called. This mechanism requires
1987 // `closestPositionFromPoint` to be implemented, to allow UIKit to translate the finger touch
1988 // location displacement to the text range to select. When the selection is completed
1989 // (i.e. when both of the 2 fingers are released), similar to "move" gesture,
1990 // the `endFloatingCursor` will be called.
1991 //
1992 // When the 2nd finger is pressed, it does not trigger another startFloatingCursor call. So
1993 // floating cursor move/selection logic has to be implemented in iOS embedder rather than
1994 // just the framework side.
1995 //
1996 // Whenever a selection is updated, the engine sends the new selection to the framework. So unlike
1997 // the move gesture, the selections in the framework and the engine are always kept in sync.
1998 - (void)beginFloatingCursorAtPoint:(CGPoint)point {
1999  // For "beginFloatingCursorAtPoint" and "updateFloatingCursorAtPoint", "point" is roughly:
2000  //
2001  // CGPoint(
2002  // width >= 0 ? point.x.clamp(boundingBox.left, boundingBox.right) : point.x,
2003  // height >= 0 ? point.y.clamp(boundingBox.top, boundingBox.bottom) : point.y,
2004  // )
2005  // where
2006  // point = keyboardPanGestureRecognizer.translationInView(textInputView) + caretRectForPosition
2007  // boundingBox = self.convertRect(bounds, fromView:textInputView)
2008  // bounds = self._selectionClipRect ?? self.bounds
2009  //
2010  // It seems impossible to use a negative "width" or "height", as the "convertRect"
2011  // call always turns a CGRect's negative dimensions into non-negative values, e.g.,
2012  // (1, 2, -3, -4) would become (-2, -2, 3, 4).
2014  _floatingCursorOffset = point;
2015  [self.textInputDelegate flutterTextInputView:self
2016  updateFloatingCursor:FlutterFloatingCursorDragStateStart
2017  withClient:_textInputClient
2018  withPosition:@{@"X" : @0, @"Y" : @0}];
2019 }
2020 
2021 - (void)updateFloatingCursorAtPoint:(CGPoint)point {
2022  [self.textInputDelegate flutterTextInputView:self
2023  updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2024  withClient:_textInputClient
2025  withPosition:@{
2026  @"X" : @(point.x - _floatingCursorOffset.x),
2027  @"Y" : @(point.y - _floatingCursorOffset.y)
2028  }];
2029 }
2030 
2031 - (void)endFloatingCursor {
2033  [self.textInputDelegate flutterTextInputView:self
2034  updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2035  withClient:_textInputClient
2036  withPosition:@{@"X" : @0, @"Y" : @0}];
2037 }
2038 
2039 #pragma mark - UIKeyInput Overrides
2040 
2041 - (void)updateEditingState {
2042  NSUInteger selectionBase = ((FlutterTextPosition*)_selectedTextRange.start).index;
2043  NSUInteger selectionExtent = ((FlutterTextPosition*)_selectedTextRange.end).index;
2044 
2045  // Empty compositing range is represented by the framework's TextRange.empty.
2046  NSInteger composingBase = -1;
2047  NSInteger composingExtent = -1;
2048  if (self.markedTextRange != nil) {
2049  composingBase = ((FlutterTextPosition*)self.markedTextRange.start).index;
2050  composingExtent = ((FlutterTextPosition*)self.markedTextRange.end).index;
2051  }
2052  NSDictionary* state = @{
2053  @"selectionBase" : @(selectionBase),
2054  @"selectionExtent" : @(selectionExtent),
2055  @"selectionAffinity" : @(_selectionAffinity),
2056  @"selectionIsDirectional" : @(false),
2057  @"composingBase" : @(composingBase),
2058  @"composingExtent" : @(composingExtent),
2059  @"text" : [NSString stringWithString:self.text],
2060  };
2061 
2062  if (_textInputClient == 0 && _autofillId != nil) {
2063  [self.textInputDelegate flutterTextInputView:self
2064  updateEditingClient:_textInputClient
2065  withState:state
2066  withTag:_autofillId];
2067  } else {
2068  [self.textInputDelegate flutterTextInputView:self
2069  updateEditingClient:_textInputClient
2070  withState:state];
2071  }
2072 }
2073 
2074 - (void)updateEditingStateWithDelta:(flutter::TextEditingDelta)delta {
2075  NSUInteger selectionBase = ((FlutterTextPosition*)_selectedTextRange.start).index;
2076  NSUInteger selectionExtent = ((FlutterTextPosition*)_selectedTextRange.end).index;
2077 
2078  // Empty compositing range is represented by the framework's TextRange.empty.
2079  NSInteger composingBase = -1;
2080  NSInteger composingExtent = -1;
2081  if (self.markedTextRange != nil) {
2082  composingBase = ((FlutterTextPosition*)self.markedTextRange.start).index;
2083  composingExtent = ((FlutterTextPosition*)self.markedTextRange.end).index;
2084  }
2085 
2086  NSDictionary* deltaToFramework = @{
2087  @"oldText" : @(delta.old_text().c_str()),
2088  @"deltaText" : @(delta.delta_text().c_str()),
2089  @"deltaStart" : @(delta.delta_start()),
2090  @"deltaEnd" : @(delta.delta_end()),
2091  @"selectionBase" : @(selectionBase),
2092  @"selectionExtent" : @(selectionExtent),
2093  @"selectionAffinity" : @(_selectionAffinity),
2094  @"selectionIsDirectional" : @(false),
2095  @"composingBase" : @(composingBase),
2096  @"composingExtent" : @(composingExtent),
2097  };
2098 
2099  [_pendingDeltas addObject:deltaToFramework];
2100 
2101  if (_pendingDeltas.count == 1) {
2102  __weak FlutterTextInputView* weakSelf = self;
2103  dispatch_async(dispatch_get_main_queue(), ^{
2104  __strong FlutterTextInputView* strongSelf = weakSelf;
2105  if (strongSelf && strongSelf.pendingDeltas.count > 0) {
2106  NSDictionary* deltas = @{
2107  @"deltas" : strongSelf.pendingDeltas,
2108  };
2109 
2110  [strongSelf.textInputDelegate flutterTextInputView:strongSelf
2111  updateEditingClient:strongSelf->_textInputClient
2112  withDelta:deltas];
2113  [strongSelf.pendingDeltas removeAllObjects];
2114  }
2115  });
2116  }
2117 }
2118 
2119 - (BOOL)hasText {
2120  return self.text.length > 0;
2121 }
2122 
2123 - (void)insertText:(NSString*)text {
2124  if (self.temporarilyDeletedComposedCharacter.length > 0 && text.length == 1 && !text.UTF8String &&
2125  [text characterAtIndex:0] == [self.temporarilyDeletedComposedCharacter characterAtIndex:0]) {
2126  // Workaround for https://github.com/flutter/flutter/issues/111494
2127  // TODO(cyanglaz): revert this workaround if when flutter supports a minimum iOS version which
2128  // this bug is fixed by Apple.
2129  text = self.temporarilyDeletedComposedCharacter;
2130  self.temporarilyDeletedComposedCharacter = nil;
2131  }
2132 
2133  NSMutableArray<FlutterTextSelectionRect*>* copiedRects =
2134  [[NSMutableArray alloc] initWithCapacity:[_selectionRects count]];
2135  NSAssert([_selectedTextRange.start isKindOfClass:[FlutterTextPosition class]],
2136  @"Expected a FlutterTextPosition for position (got %@).",
2137  [_selectedTextRange.start class]);
2138  NSUInteger insertPosition = ((FlutterTextPosition*)_selectedTextRange.start).index;
2139  for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2140  NSUInteger rectPosition = _selectionRects[i].position;
2141  if (rectPosition == insertPosition) {
2142  for (NSUInteger j = 0; j <= text.length; j++) {
2143  [copiedRects addObject:[FlutterTextSelectionRect
2144  selectionRectWithRect:_selectionRects[i].rect
2145  position:rectPosition + j
2146  writingDirection:_selectionRects[i].writingDirection]];
2147  }
2148  } else {
2149  if (rectPosition > insertPosition) {
2150  rectPosition = rectPosition + text.length;
2151  }
2152  [copiedRects addObject:[FlutterTextSelectionRect
2153  selectionRectWithRect:_selectionRects[i].rect
2154  position:rectPosition
2155  writingDirection:_selectionRects[i].writingDirection]];
2156  }
2157  }
2158 
2159  _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2160  [self resetScribbleInteractionStatusIfEnding];
2161  self.selectionRects = copiedRects;
2163  [self replaceRange:_selectedTextRange withText:text];
2164 }
2165 
2166 - (UITextPlaceholder*)insertTextPlaceholderWithSize:(CGSize)size API_AVAILABLE(ios(13.0)) {
2167  [self.textInputDelegate flutterTextInputView:self
2168  insertTextPlaceholderWithSize:size
2169  withClient:_textInputClient];
2170  _hasPlaceholder = YES;
2171  return [[FlutterTextPlaceholder alloc] init];
2172 }
2173 
2174 - (void)removeTextPlaceholder:(UITextPlaceholder*)textPlaceholder API_AVAILABLE(ios(13.0)) {
2175  _hasPlaceholder = NO;
2176  [self.textInputDelegate flutterTextInputView:self removeTextPlaceholder:_textInputClient];
2177 }
2178 
2179 - (void)deleteBackward {
2181  _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2182  [self resetScribbleInteractionStatusIfEnding];
2183 
2184  // When deleting Thai vowel, _selectedTextRange has location
2185  // but does not have length, so we have to manually set it.
2186  // In addition, we needed to delete only a part of grapheme cluster
2187  // because it is the expected behavior of Thai input.
2188  // https://github.com/flutter/flutter/issues/24203
2189  // https://github.com/flutter/flutter/issues/21745
2190  // https://github.com/flutter/flutter/issues/39399
2191  //
2192  // This is needed for correct handling of the deletion of Thai vowel input.
2193  // TODO(cbracken): Get a good understanding of expected behavior of Thai
2194  // input and ensure that this is the correct solution.
2195  // https://github.com/flutter/flutter/issues/28962
2196  if (_selectedTextRange.isEmpty && [self hasText]) {
2197  UITextRange* oldSelectedRange = _selectedTextRange;
2198  NSRange oldRange = ((FlutterTextRange*)oldSelectedRange).range;
2199  if (oldRange.location > 0) {
2200  NSRange newRange = NSMakeRange(oldRange.location - 1, 1);
2201 
2202  // We should check if the last character is a part of emoji.
2203  // If so, we must delete the entire emoji to prevent the text from being malformed.
2204  NSRange charRange = fml::RangeForCharacterAtIndex(self.text, oldRange.location - 1);
2205  if (IsEmoji(self.text, charRange)) {
2206  newRange = NSMakeRange(charRange.location, oldRange.location - charRange.location);
2207  }
2208 
2210  }
2211  }
2212 
2213  if (!_selectedTextRange.isEmpty) {
2214  // Cache the last deleted emoji to use for an iOS bug where the next
2215  // insertion corrupts the emoji characters.
2216  // See: https://github.com/flutter/flutter/issues/111494#issuecomment-1248441346
2217  if (IsEmoji(self.text, _selectedTextRange.range)) {
2218  NSString* deletedText = [self.text substringWithRange:_selectedTextRange.range];
2219  NSRange deleteFirstCharacterRange = fml::RangeForCharacterAtIndex(deletedText, 0);
2220  self.temporarilyDeletedComposedCharacter =
2221  [deletedText substringWithRange:deleteFirstCharacterRange];
2222  }
2223  [self replaceRange:_selectedTextRange withText:@""];
2224  }
2225 }
2226 
2227 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(id)target {
2228  UIAccessibilityPostNotification(notification, target);
2229 }
2230 
2231 - (void)accessibilityElementDidBecomeFocused {
2232  if ([self accessibilityElementIsFocused]) {
2233  // For most of the cases, this flutter text input view should never
2234  // receive the focus. If we do receive the focus, we make the best effort
2235  // to send the focus back to the real text field.
2236  FML_DCHECK(_backingTextInputAccessibilityObject);
2237  [self postAccessibilityNotification:UIAccessibilityScreenChangedNotification
2238  target:_backingTextInputAccessibilityObject];
2239  }
2240 }
2241 
2242 - (BOOL)accessibilityElementsHidden {
2243  return !_accessibilityEnabled;
2244 }
2245 
2247  if (_scribbleInteractionStatus == FlutterScribbleInteractionStatusEnding) {
2248  _scribbleInteractionStatus = FlutterScribbleInteractionStatusNone;
2249  }
2250 }
2251 
2252 #pragma mark - Key Events Handling
2253 - (void)pressesBegan:(NSSet<UIPress*>*)presses
2254  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2255  [_textInputPlugin.viewController pressesBegan:presses withEvent:event];
2256 }
2257 
2258 - (void)pressesChanged:(NSSet<UIPress*>*)presses
2259  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2260  [_textInputPlugin.viewController pressesChanged:presses withEvent:event];
2261 }
2262 
2263 - (void)pressesEnded:(NSSet<UIPress*>*)presses
2264  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2265  [_textInputPlugin.viewController pressesEnded:presses withEvent:event];
2266 }
2267 
2268 - (void)pressesCancelled:(NSSet<UIPress*>*)presses
2269  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2270  [_textInputPlugin.viewController pressesCancelled:presses withEvent:event];
2271 }
2272 
2273 @end
2274 
2275 /**
2276  * Hides `FlutterTextInputView` from iOS accessibility system so it
2277  * does not show up twice, once where it is in the `UIView` hierarchy,
2278  * and a second time as part of the `SemanticsObject` hierarchy.
2279  *
2280  * This prevents the `FlutterTextInputView` from receiving the focus
2281  * due to swiping gesture.
2282  *
2283  * There are other cases the `FlutterTextInputView` may receive
2284  * focus. One example is during screen changes, the accessibility
2285  * tree will undergo a dramatic structural update. The Voiceover may
2286  * decide to focus the `FlutterTextInputView` that is not involved
2287  * in the structural update instead. If that happens, the
2288  * `FlutterTextInputView` will make a best effort to direct the
2289  * focus back to the `SemanticsObject`.
2290  */
2292 }
2293 
2294 @end
2295 
2297 }
2298 
2299 - (BOOL)accessibilityElementsHidden {
2300  return YES;
2301 }
2302 
2303 @end
2304 
2305 @interface FlutterTextInputPlugin ()
2306 - (void)enableActiveViewAccessibility;
2307 @end
2308 
2309 @interface FlutterTimerProxy : NSObject
2310 @property(nonatomic, weak) FlutterTextInputPlugin* target;
2311 @end
2312 
2313 @implementation FlutterTimerProxy
2314 
2315 + (instancetype)proxyWithTarget:(FlutterTextInputPlugin*)target {
2316  FlutterTimerProxy* proxy = [[self alloc] init];
2317  if (proxy) {
2318  proxy.target = target;
2319  }
2320  return proxy;
2321 }
2322 
2323 - (void)enableActiveViewAccessibility {
2324  [self.target enableActiveViewAccessibility];
2325 }
2326 
2327 @end
2328 
2329 @interface FlutterTextInputPlugin ()
2330 // The current password-autofillable input fields that have yet to be saved.
2331 @property(nonatomic, readonly)
2332  NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
2333 @property(nonatomic, retain) FlutterTextInputView* activeView;
2334 @property(nonatomic, retain) FlutterTextInputViewAccessibilityHider* inputHider;
2335 @property(nonatomic, readonly, weak) id<FlutterViewResponder> viewResponder;
2336 
2337 @property(nonatomic, strong) UIView* keyboardViewContainer;
2338 @property(nonatomic, strong) UIView* keyboardView;
2339 @property(nonatomic, strong) UIView* cachedFirstResponder;
2340 @property(nonatomic, assign) CGRect keyboardRect;
2341 @property(nonatomic, assign) CGFloat previousPointerYPosition;
2342 @property(nonatomic, assign) CGFloat pointerYVelocity;
2343 @end
2344 
2345 @implementation FlutterTextInputPlugin {
2346  NSTimer* _enableFlutterTextInputViewAccessibilityTimer;
2347 }
2348 
2349 - (instancetype)initWithDelegate:(id<FlutterTextInputDelegate>)textInputDelegate {
2350  self = [super init];
2351  if (self) {
2352  // `_textInputDelegate` is a weak reference because it should retain FlutterTextInputPlugin.
2353  _textInputDelegate = textInputDelegate;
2354  _autofillContext = [[NSMutableDictionary alloc] init];
2355  _inputHider = [[FlutterTextInputViewAccessibilityHider alloc] init];
2356  _scribbleElements = [[NSMutableDictionary alloc] init];
2357  _keyboardViewContainer = [[UIView alloc] init];
2358 
2359  [[NSNotificationCenter defaultCenter] addObserver:self
2360  selector:@selector(handleKeyboardWillShow:)
2361  name:UIKeyboardWillShowNotification
2362  object:nil];
2363  }
2364 
2365  return self;
2366 }
2367 
2368 - (void)handleKeyboardWillShow:(NSNotification*)notification {
2369  NSDictionary* keyboardInfo = [notification userInfo];
2370  NSValue* keyboardFrameEnd = [keyboardInfo valueForKey:UIKeyboardFrameEndUserInfoKey];
2371  _keyboardRect = [keyboardFrameEnd CGRectValue];
2372 }
2373 
2374 - (void)dealloc {
2375  [self hideTextInput];
2376 }
2377 
2378 - (void)removeEnableFlutterTextInputViewAccessibilityTimer {
2379  if (_enableFlutterTextInputViewAccessibilityTimer) {
2380  [_enableFlutterTextInputViewAccessibilityTimer invalidate];
2381  _enableFlutterTextInputViewAccessibilityTimer = nil;
2382  }
2383 }
2384 
2385 - (UIView<UITextInput>*)textInputView {
2386  return _activeView;
2387 }
2388 
2389 - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
2390  NSString* method = call.method;
2391  id args = call.arguments;
2392  if ([method isEqualToString:kShowMethod]) {
2393  [self showTextInput];
2394  result(nil);
2395  } else if ([method isEqualToString:kHideMethod]) {
2396  [self hideTextInput];
2397  result(nil);
2398  } else if ([method isEqualToString:kSetClientMethod]) {
2399  [self setTextInputClient:[args[0] intValue] withConfiguration:args[1]];
2400  result(nil);
2401  } else if ([method isEqualToString:kSetPlatformViewClientMethod]) {
2402  // This method call has a `platformViewId` argument, but we do not need it for iOS for now.
2403  [self setPlatformViewTextInputClient];
2404  result(nil);
2405  } else if ([method isEqualToString:kSetEditingStateMethod]) {
2406  [self setTextInputEditingState:args];
2407  result(nil);
2408  } else if ([method isEqualToString:kClearClientMethod]) {
2409  [self clearTextInputClient];
2410  result(nil);
2411  } else if ([method isEqualToString:kSetEditableSizeAndTransformMethod]) {
2412  [self setEditableSizeAndTransform:args];
2413  result(nil);
2414  } else if ([method isEqualToString:kSetMarkedTextRectMethod]) {
2415  [self updateMarkedRect:args];
2416  result(nil);
2417  } else if ([method isEqualToString:kFinishAutofillContextMethod]) {
2418  [self triggerAutofillSave:[args boolValue]];
2419  result(nil);
2420  // TODO(justinmc): Remove the TextInput method constant when the framework has
2421  // finished transitioning to using the Scribble channel.
2422  // https://github.com/flutter/flutter/pull/104128
2423  } else if ([method isEqualToString:kDeprecatedSetSelectionRectsMethod]) {
2424  [self setSelectionRects:args];
2425  result(nil);
2426  } else if ([method isEqualToString:kSetSelectionRectsMethod]) {
2427  [self setSelectionRects:args];
2428  result(nil);
2429  } else if ([method isEqualToString:kStartLiveTextInputMethod]) {
2430  [self startLiveTextInput];
2431  result(nil);
2432  } else if ([method isEqualToString:kUpdateConfigMethod]) {
2433  [self updateConfig:args];
2434  result(nil);
2435  } else if ([method isEqualToString:kOnInteractiveKeyboardPointerMoveMethod]) {
2436  CGFloat pointerY = (CGFloat)[args[@"pointerY"] doubleValue];
2437  [self handlePointerMove:pointerY];
2438  result(nil);
2439  } else if ([method isEqualToString:kOnInteractiveKeyboardPointerUpMethod]) {
2440  CGFloat pointerY = (CGFloat)[args[@"pointerY"] doubleValue];
2441  [self handlePointerUp:pointerY];
2442  result(nil);
2443  } else {
2445  }
2446 }
2447 
2448 - (void)handlePointerUp:(CGFloat)pointerY {
2449  if (_keyboardView.superview != nil) {
2450  // Done to avoid the issue of a pointer up done without a screenshot
2451  // View must be loaded at this point.
2452  UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2453  CGFloat screenHeight = screen.bounds.size.height;
2454  CGFloat keyboardHeight = _keyboardRect.size.height;
2455  // Negative velocity indicates a downward movement
2456  BOOL shouldDismissKeyboardBasedOnVelocity = _pointerYVelocity < 0;
2457  [UIView animateWithDuration:kKeyboardAnimationTimeToCompleteion
2458  animations:^{
2459  double keyboardDestination =
2460  shouldDismissKeyboardBasedOnVelocity ? screenHeight : screenHeight - keyboardHeight;
2461  _keyboardViewContainer.frame = CGRectMake(
2462  0, keyboardDestination, _viewController.flutterScreenIfViewLoaded.bounds.size.width,
2463  _keyboardViewContainer.frame.size.height);
2464  }
2465  completion:^(BOOL finished) {
2466  if (shouldDismissKeyboardBasedOnVelocity) {
2467  [self.textInputDelegate flutterTextInputView:self.activeView
2468  didResignFirstResponderWithTextInputClient:self.activeView.textInputClient];
2469  [self dismissKeyboardScreenshot];
2470  } else {
2471  [self showKeyboardAndRemoveScreenshot];
2472  }
2473  }];
2474  }
2475 }
2476 
2477 - (void)dismissKeyboardScreenshot {
2478  for (UIView* subView in _keyboardViewContainer.subviews) {
2479  [subView removeFromSuperview];
2480  }
2481 }
2482 
2483 - (void)showKeyboardAndRemoveScreenshot {
2484  [UIView setAnimationsEnabled:NO];
2485  [_cachedFirstResponder becomeFirstResponder];
2486  // UIKit does not immediately access the areAnimationsEnabled Boolean so a delay is needed before
2487  // returned
2488  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kKeyboardAnimationDelaySeconds * NSEC_PER_SEC),
2489  dispatch_get_main_queue(), ^{
2490  [UIView setAnimationsEnabled:YES];
2491  [self dismissKeyboardScreenshot];
2492  });
2493 }
2494 
2495 - (void)handlePointerMove:(CGFloat)pointerY {
2496  // View must be loaded at this point.
2497  UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2498  CGFloat screenHeight = screen.bounds.size.height;
2499  CGFloat keyboardHeight = _keyboardRect.size.height;
2500  if (screenHeight - keyboardHeight <= pointerY) {
2501  // If the pointer is within the bounds of the keyboard.
2502  if (_keyboardView.superview == nil) {
2503  // If no screenshot has been taken.
2504  [self takeKeyboardScreenshotAndDisplay];
2505  [self hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate];
2506  } else {
2507  [self setKeyboardContainerHeight:pointerY];
2508  _pointerYVelocity = _previousPointerYPosition - pointerY;
2509  }
2510  } else {
2511  if (_keyboardView.superview != nil) {
2512  // Keeps keyboard at proper height.
2513  _keyboardViewContainer.frame = _keyboardRect;
2514  _pointerYVelocity = _previousPointerYPosition - pointerY;
2515  }
2516  }
2517  _previousPointerYPosition = pointerY;
2518 }
2519 
2520 - (void)setKeyboardContainerHeight:(CGFloat)pointerY {
2521  CGRect frameRect = _keyboardRect;
2522  frameRect.origin.y = pointerY;
2523  _keyboardViewContainer.frame = frameRect;
2524 }
2525 
2526 - (void)hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate {
2527  [UIView setAnimationsEnabled:NO];
2528  _cachedFirstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
2529  _activeView.preventCursorDismissWhenResignFirstResponder = YES;
2530  [_cachedFirstResponder resignFirstResponder];
2531  _activeView.preventCursorDismissWhenResignFirstResponder = NO;
2532  [UIView setAnimationsEnabled:YES];
2533 }
2534 
2535 - (void)takeKeyboardScreenshotAndDisplay {
2536  // View must be loaded at this point
2537  UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2538  UIView* keyboardSnap = [screen snapshotViewAfterScreenUpdates:YES];
2539  keyboardSnap = [keyboardSnap resizableSnapshotViewFromRect:_keyboardRect
2540  afterScreenUpdates:YES
2541  withCapInsets:UIEdgeInsetsZero];
2542  _keyboardView = keyboardSnap;
2543  [_keyboardViewContainer addSubview:_keyboardView];
2544  if (_keyboardViewContainer.superview == nil) {
2545  [UIApplication.sharedApplication.delegate.window.rootViewController.view
2546  addSubview:_keyboardViewContainer];
2547  }
2548  _keyboardViewContainer.layer.zPosition = NSIntegerMax;
2549  _keyboardViewContainer.frame = _keyboardRect;
2550 }
2551 
2552 - (BOOL)showEditMenu:(NSDictionary*)args API_AVAILABLE(ios(16.0)) {
2553  if (!self.activeView.isFirstResponder) {
2554  return NO;
2555  }
2556  NSDictionary<NSString*, NSNumber*>* encodedTargetRect = args[@"targetRect"];
2557  CGRect globalTargetRect = CGRectMake(
2558  [encodedTargetRect[@"x"] doubleValue], [encodedTargetRect[@"y"] doubleValue],
2559  [encodedTargetRect[@"width"] doubleValue], [encodedTargetRect[@"height"] doubleValue]);
2560  CGRect localTargetRect = [self.hostView convertRect:globalTargetRect toView:self.activeView];
2561  [self.activeView showEditMenuWithTargetRect:localTargetRect];
2562  return YES;
2563 }
2564 
2565 - (void)hideEditMenu {
2566  [self.activeView hideEditMenu];
2567 }
2568 
2569 - (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
2570  NSArray* transform = dictionary[@"transform"];
2571  [_activeView setEditableTransform:transform];
2572  const int leftIndex = 12;
2573  const int topIndex = 13;
2574  if ([_activeView isScribbleAvailable]) {
2575  // This is necessary to set up where the scribble interactable element will be.
2576  _inputHider.frame =
2577  CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue],
2578  [dictionary[@"width"] intValue], [dictionary[@"height"] intValue]);
2579  _activeView.frame =
2580  CGRectMake(0, 0, [dictionary[@"width"] intValue], [dictionary[@"height"] intValue]);
2581  _activeView.tintColor = [UIColor clearColor];
2582  } else {
2583  // TODO(hellohuanlin): Also need to handle iOS 16 case, where the auto-correction highlight does
2584  // not match the size of text.
2585  // See https://github.com/flutter/flutter/issues/131695
2586  if (@available(iOS 17, *)) {
2587  // Move auto-correction highlight to overlap with the actual text.
2588  // This is to fix an issue where the system auto-correction highlight is displayed at
2589  // the top left corner of the screen on iOS 17+.
2590  // This problem also happens on iOS 16, but the size of highlight does not match the text.
2591  // See https://github.com/flutter/flutter/issues/131695
2592  // TODO(hellohuanlin): Investigate if we can use non-zero size.
2593  _inputHider.frame =
2594  CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue], 0, 0);
2595  }
2596  }
2597 }
2598 
2599 - (void)updateMarkedRect:(NSDictionary*)dictionary {
2600  NSAssert(dictionary[@"x"] != nil && dictionary[@"y"] != nil && dictionary[@"width"] != nil &&
2601  dictionary[@"height"] != nil,
2602  @"Expected a dictionary representing a CGRect, got %@", dictionary);
2603  CGRect rect = CGRectMake([dictionary[@"x"] doubleValue], [dictionary[@"y"] doubleValue],
2604  [dictionary[@"width"] doubleValue], [dictionary[@"height"] doubleValue]);
2605  _activeView.markedRect = rect.size.width < 0 && rect.size.height < 0 ? kInvalidFirstRect : rect;
2606 }
2607 
2608 - (void)setSelectionRects:(NSArray*)encodedRects {
2609  NSMutableArray<FlutterTextSelectionRect*>* rectsAsRect =
2610  [[NSMutableArray alloc] initWithCapacity:[encodedRects count]];
2611  for (NSUInteger i = 0; i < [encodedRects count]; i++) {
2612  NSArray<NSNumber*>* encodedRect = encodedRects[i];
2613  [rectsAsRect addObject:[FlutterTextSelectionRect
2614  selectionRectWithRect:CGRectMake([encodedRect[0] floatValue],
2615  [encodedRect[1] floatValue],
2616  [encodedRect[2] floatValue],
2617  [encodedRect[3] floatValue])
2618  position:[encodedRect[4] unsignedIntegerValue]
2619  writingDirection:[encodedRect[5] unsignedIntegerValue] == 1
2620  ? NSWritingDirectionLeftToRight
2621  : NSWritingDirectionRightToLeft]];
2622  }
2623 
2624  // TODO(hellohuanlin): Investigate why notifying the text input system about text changes (via
2625  // textWillChange and textDidChange APIs) causes a bug where we cannot enter text with IME
2626  // keyboards. Issue: https://github.com/flutter/flutter/issues/133908
2627  _activeView.selectionRects = rectsAsRect;
2628 }
2629 
2630 - (void)startLiveTextInput {
2631  if (@available(iOS 15.0, *)) {
2632  if (_activeView == nil || !_activeView.isFirstResponder) {
2633  return;
2634  }
2635  [_activeView captureTextFromCamera:nil];
2636  }
2637 }
2638 
2639 - (void)showTextInput {
2640  _activeView.viewResponder = _viewResponder;
2641  [self addToInputParentViewIfNeeded:_activeView];
2642  // Adds a delay to prevent the text view from receiving accessibility
2643  // focus in case it is activated during semantics updates.
2644  //
2645  // One common case is when the app navigates to a page with an auto
2646  // focused text field. The text field will activate the FlutterTextInputView
2647  // with a semantics update sent to the engine. The voiceover will focus
2648  // the newly attached active view while performing accessibility update.
2649  // This results in accessibility focus stuck at the FlutterTextInputView.
2650  if (!_enableFlutterTextInputViewAccessibilityTimer) {
2651  _enableFlutterTextInputViewAccessibilityTimer =
2652  [NSTimer scheduledTimerWithTimeInterval:kUITextInputAccessibilityEnablingDelaySeconds
2653  target:[FlutterTimerProxy proxyWithTarget:self]
2654  selector:@selector(enableActiveViewAccessibility)
2655  userInfo:nil
2656  repeats:NO];
2657  }
2658  [_activeView becomeFirstResponder];
2659 }
2660 
2661 - (void)enableActiveViewAccessibility {
2662  if (_activeView.isFirstResponder) {
2663  _activeView.accessibilityEnabled = YES;
2664  }
2665  [self removeEnableFlutterTextInputViewAccessibilityTimer];
2666 }
2667 
2668 - (void)hideTextInput {
2669  [self removeEnableFlutterTextInputViewAccessibilityTimer];
2670  _activeView.accessibilityEnabled = NO;
2671  [_activeView resignFirstResponder];
2672  [_activeView removeFromSuperview];
2673  [_inputHider removeFromSuperview];
2674 }
2675 
2676 - (void)triggerAutofillSave:(BOOL)saveEntries {
2677  [_activeView resignFirstResponder];
2678 
2679  if (saveEntries) {
2680  // Make all the input fields in the autofill context visible,
2681  // then remove them to trigger autofill save.
2682  [self cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
2683  [_autofillContext removeAllObjects];
2684  [self changeInputViewsAutofillVisibility:YES];
2685  } else {
2686  [_autofillContext removeAllObjects];
2687  }
2688 
2689  [self cleanUpViewHierarchy:YES clearText:!saveEntries delayRemoval:NO];
2690  [self addToInputParentViewIfNeeded:_activeView];
2691 }
2692 
2693 - (void)setPlatformViewTextInputClient {
2694  // No need to track the platformViewID (unlike in Android). When a platform view
2695  // becomes the first responder, simply hide this dummy text input view (`_activeView`)
2696  // for the previously focused widget.
2697  [self removeEnableFlutterTextInputViewAccessibilityTimer];
2698  _activeView.accessibilityEnabled = NO;
2699  [_activeView removeFromSuperview];
2700  [_inputHider removeFromSuperview];
2701 }
2702 
2703 - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configuration {
2704  [self resetAllClientIds];
2705  // Hide all input views from autofill, only make those in the new configuration visible
2706  // to autofill.
2707  [self changeInputViewsAutofillVisibility:NO];
2708 
2709  // Update the current active view.
2710  switch (AutofillTypeOf(configuration)) {
2711  case kFlutterAutofillTypeNone:
2712  self.activeView = [self createInputViewWith:configuration];
2713  break;
2714  case kFlutterAutofillTypeRegular:
2715  // If the group does not involve password autofill, only install the
2716  // input view that's being focused.
2717  self.activeView = [self updateAndShowAutofillViews:nil
2718  focusedField:configuration
2719  isPasswordRelated:NO];
2720  break;
2721  case kFlutterAutofillTypePassword:
2722  self.activeView = [self updateAndShowAutofillViews:configuration[kAssociatedAutofillFields]
2723  focusedField:configuration
2724  isPasswordRelated:YES];
2725  break;
2726  }
2727  [_activeView setTextInputClient:client];
2728  [_activeView reloadInputViews];
2729 
2730  // Clean up views that no longer need to be in the view hierarchy, according to
2731  // the current autofill context. The "garbage" input views are already made
2732  // invisible to autofill and they can't `becomeFirstResponder`, we only remove
2733  // them to free up resources and reduce the number of input views in the view
2734  // hierarchy.
2735  //
2736  // The garbage views are decommissioned immediately, but the removeFromSuperview
2737  // call is scheduled on the runloop and delayed by 0.1s so we don't remove the
2738  // text fields immediately (which seems to make the keyboard flicker).
2739  // See: https://github.com/flutter/flutter/issues/64628.
2740  [self cleanUpViewHierarchy:NO clearText:YES delayRemoval:YES];
2741 }
2742 
2743 // Creates and shows an input field that is not password related and has no autofill
2744 // info. This method returns a new FlutterTextInputView instance when called, since
2745 // UIKit uses the identity of `UITextInput` instances (or the identity of the input
2746 // views) to decide whether the IME's internal states should be reset. See:
2747 // https://github.com/flutter/flutter/issues/79031 .
2748 - (FlutterTextInputView*)createInputViewWith:(NSDictionary*)configuration {
2749  NSString* autofillId = AutofillIdFromDictionary(configuration);
2750  if (autofillId) {
2751  [_autofillContext removeObjectForKey:autofillId];
2752  }
2753  FlutterTextInputView* newView = [[FlutterTextInputView alloc] initWithOwner:self];
2754  [newView configureWithDictionary:configuration];
2755  [self addToInputParentViewIfNeeded:newView];
2756 
2757  for (NSDictionary* field in configuration[kAssociatedAutofillFields]) {
2758  NSString* autofillId = AutofillIdFromDictionary(field);
2759  if (autofillId && AutofillTypeOf(field) == kFlutterAutofillTypeNone) {
2760  [_autofillContext removeObjectForKey:autofillId];
2761  }
2762  }
2763  return newView;
2764 }
2765 
2766 - (FlutterTextInputView*)updateAndShowAutofillViews:(NSArray*)fields
2767  focusedField:(NSDictionary*)focusedField
2768  isPasswordRelated:(BOOL)isPassword {
2769  FlutterTextInputView* focused = nil;
2770  NSString* focusedId = AutofillIdFromDictionary(focusedField);
2771  NSAssert(focusedId, @"autofillId must not be null for the focused field: %@", focusedField);
2772 
2773  if (!fields) {
2774  // DO NOT push the current autofillable input fields to the context even
2775  // if it's password-related, because it is not in an autofill group.
2776  focused = [self getOrCreateAutofillableView:focusedField isPasswordAutofill:isPassword];
2777  [_autofillContext removeObjectForKey:focusedId];
2778  }
2779 
2780  for (NSDictionary* field in fields) {
2781  NSString* autofillId = AutofillIdFromDictionary(field);
2782  NSAssert(autofillId, @"autofillId must not be null for field: %@", field);
2783 
2784  BOOL hasHints = AutofillTypeOf(field) != kFlutterAutofillTypeNone;
2785  BOOL isFocused = [focusedId isEqualToString:autofillId];
2786 
2787  if (isFocused) {
2788  focused = [self getOrCreateAutofillableView:field isPasswordAutofill:isPassword];
2789  }
2790 
2791  if (hasHints) {
2792  // Push the current input field to the context if it has hints.
2793  _autofillContext[autofillId] = isFocused ? focused
2794  : [self getOrCreateAutofillableView:field
2795  isPasswordAutofill:isPassword];
2796  } else {
2797  // Mark for deletion.
2798  [_autofillContext removeObjectForKey:autofillId];
2799  }
2800  }
2801 
2802  NSAssert(focused, @"The current focused input view must not be nil.");
2803  return focused;
2804 }
2805 
2806 // Returns a new non-reusable input view (and put it into the view hierarchy), or get the
2807 // view from the current autofill context, if an input view with the same autofill id
2808 // already exists in the context.
2809 // This is generally used for input fields that are autofillable (UIKit tracks these veiws
2810 // for autofill purposes so they should not be reused for a different type of views).
2811 - (FlutterTextInputView*)getOrCreateAutofillableView:(NSDictionary*)field
2812  isPasswordAutofill:(BOOL)needsPasswordAutofill {
2813  NSString* autofillId = AutofillIdFromDictionary(field);
2814  FlutterTextInputView* inputView = _autofillContext[autofillId];
2815  if (!inputView) {
2816  inputView =
2817  needsPasswordAutofill ? [FlutterSecureTextInputView alloc] : [FlutterTextInputView alloc];
2818  inputView = [inputView initWithOwner:self];
2819  [self addToInputParentViewIfNeeded:inputView];
2820  }
2821 
2822  [inputView configureWithDictionary:field];
2823  return inputView;
2824 }
2825 
2826 // The UIView to add FlutterTextInputViews to.
2827 - (UIView*)hostView {
2828  UIView* host = _viewController.view;
2829  NSAssert(host != nullptr,
2830  @"The application must have a host view since the keyboard client "
2831  @"must be part of the responder chain to function. The host view controller is %@",
2832  _viewController);
2833  return host;
2834 }
2835 
2836 // The UIView to add FlutterTextInputViews to.
2837 - (NSArray<UIView*>*)textInputViews {
2838  return _inputHider.subviews;
2839 }
2840 
2841 // Removes every installed input field, unless it's in the current autofill context.
2842 //
2843 // The active view will be removed from its superview too, if includeActiveView is YES.
2844 // When clearText is YES, the text on the input fields will be set to empty before
2845 // they are removed from the view hierarchy, to avoid triggering autofill save.
2846 // If delayRemoval is true, removeFromSuperview will be scheduled on the runloop and
2847 // will be delayed by 0.1s so we don't remove the text fields immediately (which seems
2848 // to make the keyboard flicker).
2849 // See: https://github.com/flutter/flutter/issues/64628.
2850 
2851 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
2852  clearText:(BOOL)clearText
2853  delayRemoval:(BOOL)delayRemoval {
2854  for (UIView* view in self.textInputViews) {
2855  if ([view isKindOfClass:[FlutterTextInputView class]] &&
2856  (includeActiveView || view != _activeView)) {
2857  FlutterTextInputView* inputView = (FlutterTextInputView*)view;
2858  if (_autofillContext[inputView.autofillId] != view) {
2859  if (clearText) {
2860  [inputView replaceRangeLocal:NSMakeRange(0, inputView.text.length) withText:@""];
2861  }
2862  if (delayRemoval) {
2863  [inputView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:0.1];
2864  } else {
2865  [inputView removeFromSuperview];
2866  }
2867  }
2868  }
2869  }
2870 }
2871 
2872 // Changes the visibility of every FlutterTextInputView currently in the
2873 // view hierarchy.
2874 - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
2875  for (UIView* view in self.textInputViews) {
2876  if ([view isKindOfClass:[FlutterTextInputView class]]) {
2877  FlutterTextInputView* inputView = (FlutterTextInputView*)view;
2878  inputView.isVisibleToAutofill = newVisibility;
2879  }
2880  }
2881 }
2882 
2883 // Resets the client id of every FlutterTextInputView in the view hierarchy
2884 // to 0.
2885 // Called before establishing a new text input connection.
2886 // For views in the current autofill context, they need to
2887 // stay in the view hierachy but should not be allowed to
2888 // send messages (other than autofill related ones) to the
2889 // framework.
2890 - (void)resetAllClientIds {
2891  for (UIView* view in self.textInputViews) {
2892  if ([view isKindOfClass:[FlutterTextInputView class]]) {
2893  FlutterTextInputView* inputView = (FlutterTextInputView*)view;
2894  [inputView setTextInputClient:0];
2895  }
2896  }
2897 }
2898 
2899 - (void)addToInputParentViewIfNeeded:(FlutterTextInputView*)inputView {
2900  if (![inputView isDescendantOfView:_inputHider]) {
2901  [_inputHider addSubview:inputView];
2902  }
2903 
2904  if (_viewController.view == nil) {
2905  // If view controller's view has detached from flutter engine, we don't add _inputHider
2906  // in parent view to fallback and avoid crash.
2907  // https://github.com/flutter/flutter/issues/106404.
2908  return;
2909  }
2910  UIView* parentView = self.hostView;
2911  if (_inputHider.superview != parentView) {
2912  [parentView addSubview:_inputHider];
2913  }
2914 }
2915 
2916 - (void)setTextInputEditingState:(NSDictionary*)state {
2917  [_activeView setTextInputState:state];
2918 }
2919 
2920 - (void)clearTextInputClient {
2921  [_activeView setTextInputClient:0];
2922  _activeView.frame = CGRectZero;
2923 }
2924 
2925 - (void)updateConfig:(NSDictionary*)dictionary {
2926  BOOL isSecureTextEntry = [dictionary[kSecureTextEntry] boolValue];
2927  for (UIView* view in self.textInputViews) {
2928  if ([view isKindOfClass:[FlutterTextInputView class]]) {
2929  FlutterTextInputView* inputView = (FlutterTextInputView*)view;
2930  // The feature of holding and draging spacebar to move cursor is affected by
2931  // secureTextEntry, so when obscureText is updated, we need to update secureTextEntry
2932  // and call reloadInputViews.
2933  // https://github.com/flutter/flutter/issues/122139
2934  if (inputView.isSecureTextEntry != isSecureTextEntry) {
2935  inputView.secureTextEntry = isSecureTextEntry;
2936  [inputView reloadInputViews];
2937  }
2938  }
2939  }
2940 }
2941 
2942 #pragma mark UIIndirectScribbleInteractionDelegate
2943 
2944 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2945  isElementFocused:(UIScribbleElementIdentifier)elementIdentifier
2946  API_AVAILABLE(ios(14.0)) {
2947  return _activeView.scribbleFocusStatus == FlutterScribbleFocusStatusFocused;
2948 }
2949 
2950 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2951  focusElementIfNeeded:(UIScribbleElementIdentifier)elementIdentifier
2952  referencePoint:(CGPoint)focusReferencePoint
2953  completion:(void (^)(UIResponder<UITextInput>* focusedInput))completion
2954  API_AVAILABLE(ios(14.0)) {
2955  _activeView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
2956  [_indirectScribbleDelegate flutterTextInputPlugin:self
2957  focusElement:elementIdentifier
2958  atPoint:focusReferencePoint
2959  result:^(id _Nullable result) {
2960  _activeView.scribbleFocusStatus =
2961  FlutterScribbleFocusStatusFocused;
2962  completion(_activeView);
2963  }];
2964 }
2965 
2966 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2967  shouldDelayFocusForElement:(UIScribbleElementIdentifier)elementIdentifier
2968  API_AVAILABLE(ios(14.0)) {
2969  return NO;
2970 }
2971 
2972 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2973  willBeginWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
2974  API_AVAILABLE(ios(14.0)) {
2975 }
2976 
2977 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2978  didFinishWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
2979  API_AVAILABLE(ios(14.0)) {
2980 }
2981 
2982 - (CGRect)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2983  frameForElement:(UIScribbleElementIdentifier)elementIdentifier
2984  API_AVAILABLE(ios(14.0)) {
2985  NSValue* elementValue = [_scribbleElements objectForKey:elementIdentifier];
2986  if (elementValue == nil) {
2987  return CGRectZero;
2988  }
2989  return [elementValue CGRectValue];
2990 }
2991 
2992 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2993  requestElementsInRect:(CGRect)rect
2994  completion:
2995  (void (^)(NSArray<UIScribbleElementIdentifier>* elements))completion
2996  API_AVAILABLE(ios(14.0)) {
2997  [_indirectScribbleDelegate
2998  flutterTextInputPlugin:self
2999  requestElementsInRect:rect
3000  result:^(id _Nullable result) {
3001  NSMutableArray<UIScribbleElementIdentifier>* elements =
3002  [[NSMutableArray alloc] init];
3003  if ([result isKindOfClass:[NSArray class]]) {
3004  for (NSArray* elementArray in result) {
3005  [elements addObject:elementArray[0]];
3006  [_scribbleElements
3007  setObject:[NSValue
3008  valueWithCGRect:CGRectMake(
3009  [elementArray[1] floatValue],
3010  [elementArray[2] floatValue],
3011  [elementArray[3] floatValue],
3012  [elementArray[4] floatValue])]
3013  forKey:elementArray[0]];
3014  }
3015  }
3016  completion(elements);
3017  }];
3018 }
3019 
3020 #pragma mark - Methods related to Scribble support
3021 
3022 - (void)setUpIndirectScribbleInteraction:(id<FlutterViewResponder>)viewResponder {
3023  if (_viewResponder != viewResponder) {
3024  if (@available(iOS 14.0, *)) {
3025  UIView* parentView = viewResponder.view;
3026  if (parentView != nil) {
3027  UIIndirectScribbleInteraction* scribbleInteraction = [[UIIndirectScribbleInteraction alloc]
3028  initWithDelegate:(id<UIIndirectScribbleInteractionDelegate>)self];
3029  [parentView addInteraction:scribbleInteraction];
3030  }
3031  }
3032  }
3033  _viewResponder = viewResponder;
3034 }
3035 
3036 - (void)resetViewResponder {
3037  _viewResponder = nil;
3038 }
3039 
3040 #pragma mark -
3041 #pragma mark FlutterKeySecondaryResponder
3042 
3043 /**
3044  * Handles key down events received from the view controller, responding YES if
3045  * the event was handled.
3046  */
3047 - (BOOL)handlePress:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4)) {
3048  return NO;
3049 }
3050 @end
3051 
3052 /**
3053  * Recursively searches the UIView's subviews to locate the First Responder
3054  */
3055 @implementation UIView (FindFirstResponder)
3056 - (id)flutterFirstResponder {
3057  if (self.isFirstResponder) {
3058  return self;
3059  }
3060  for (UIView* subView in self.subviews) {
3061  UIView* firstResponder = subView.flutterFirstResponder;
3062  if (firstResponder) {
3063  return firstResponder;
3064  }
3065  }
3066  return nil;
3067 }
3068 @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:122
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:132
_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:2309
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
_textField
UITextField * _textField
Definition: FlutterPlatformPlugin.mm:76
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:2291
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:2310
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