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