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