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