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