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