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