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