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