| 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 if ([inputType isEqualToString:@"TextInputType.visiblePassword"]) {
141 return UIKeyboardTypeASCIICapable;
142 }
143 return UIKeyboardTypeDefault;
144 }
145
146 static UITextAutocapitalizationType ToUITextAutoCapitalizationType(NSDictionary* type) {
147 NSString* textCapitalization = type[@"textCapitalization"];
148 if ([textCapitalization isEqualToString:@"TextCapitalization.characters"]) {
149 return UITextAutocapitalizationTypeAllCharacters;
150 } else if ([textCapitalization isEqualToString:@"TextCapitalization.sentences"]) {
151 return UITextAutocapitalizationTypeSentences;
152 } else if ([textCapitalization isEqualToString:@"TextCapitalization.words"]) {
153 return UITextAutocapitalizationTypeWords;
154 }
155 return UITextAutocapitalizationTypeNone;
156 }
157
158 static UIReturnKeyType ToUIReturnKeyType(NSString* inputType) {
159 // Where did the term "unspecified" come from? iOS has a "default" and Android
160 // has "unspecified." These 2 terms seem to mean the same thing but we need
161 // to pick just one. "unspecified" was chosen because "default" is often a
162 // reserved word in languages with switch statements (dart, java, etc).
163 if ([inputType isEqualToString:@"TextInputAction.unspecified"]) {
164 return UIReturnKeyDefault;
165 }
166
167 if ([inputType isEqualToString:@"TextInputAction.done"]) {
168 return UIReturnKeyDone;
169 }
170
171 if ([inputType isEqualToString:@"TextInputAction.go"]) {
172 return UIReturnKeyGo;
173 }
174
175 if ([inputType isEqualToString:@"TextInputAction.send"]) {
176 return UIReturnKeySend;
177 }
178
179 if ([inputType isEqualToString:@"TextInputAction.search"]) {
180 return UIReturnKeySearch;
181 }
182
183 if ([inputType isEqualToString:@"TextInputAction.next"]) {
184 return UIReturnKeyNext;
185 }
186
187 if ([inputType isEqualToString:@"TextInputAction.continueAction"]) {
188 return UIReturnKeyContinue;
189 }
190
191 if ([inputType isEqualToString:@"TextInputAction.join"]) {
192 return UIReturnKeyJoin;
193 }
194
195 if ([inputType isEqualToString:@"TextInputAction.route"]) {
196 return UIReturnKeyRoute;
197 }
198
199 if ([inputType isEqualToString:@"TextInputAction.emergencyCall"]) {
200 return UIReturnKeyEmergencyCall;
201 }
202
203 if ([inputType isEqualToString:@"TextInputAction.newline"]) {
204 return UIReturnKeyDefault;
205 }
206
207 // Present default key if bad input type is given.
208 return UIReturnKeyDefault;
209 }
210
211 static UITextContentType ToUITextContentType(NSArray<NSString*>* hints) {
212 if (!hints || hints.count == 0) {
213 // If no hints are specified, use the default content type nil.
214 return nil;
215 }
216
217 NSString* hint = hints[0];
218 if ([hint isEqualToString:@"addressCityAndState"]) {
219 return UITextContentTypeAddressCityAndState;
220 }
221
222 if ([hint isEqualToString:@"addressState"]) {
223 return UITextContentTypeAddressState;
224 }
225
226 if ([hint isEqualToString:@"addressCity"]) {
227 return UITextContentTypeAddressCity;
228 }
229
230 if ([hint isEqualToString:@"sublocality"]) {
231 return UITextContentTypeSublocality;
232 }
233
234 if ([hint isEqualToString:@"streetAddressLine1"]) {
235 return UITextContentTypeStreetAddressLine1;
236 }
237
238 if ([hint isEqualToString:@"streetAddressLine2"]) {
239 return UITextContentTypeStreetAddressLine2;
240 }
241
242 if ([hint isEqualToString:@"countryName"]) {
243 return UITextContentTypeCountryName;
244 }
245
246 if ([hint isEqualToString:@"fullStreetAddress"]) {
247 return UITextContentTypeFullStreetAddress;
248 }
249
250 if ([hint isEqualToString:@"postalCode"]) {
251 return UITextContentTypePostalCode;
252 }
253
254 if ([hint isEqualToString:@"location"]) {
255 return UITextContentTypeLocation;
256 }
257
258 if ([hint isEqualToString:@"creditCardNumber"]) {
259 return UITextContentTypeCreditCardNumber;
260 }
261
262 if ([hint isEqualToString:@"email"]) {
263 return UITextContentTypeEmailAddress;
264 }
265
266 if ([hint isEqualToString:@"jobTitle"]) {
267 return UITextContentTypeJobTitle;
268 }
269
270 if ([hint isEqualToString:@"givenName"]) {
271 return UITextContentTypeGivenName;
272 }
273
274 if ([hint isEqualToString:@"middleName"]) {
275 return UITextContentTypeMiddleName;
276 }
277
278 if ([hint isEqualToString:@"familyName"]) {
279 return UITextContentTypeFamilyName;
280 }
281
282 if ([hint isEqualToString:@"name"]) {
283 return UITextContentTypeName;
284 }
285
286 if ([hint isEqualToString:@"namePrefix"]) {
287 return UITextContentTypeNamePrefix;
288 }
289
290 if ([hint isEqualToString:@"nameSuffix"]) {
291 return UITextContentTypeNameSuffix;
292 }
293
294 if ([hint isEqualToString:@"nickname"]) {
295 return UITextContentTypeNickname;
296 }
297
298 if ([hint isEqualToString:@"organizationName"]) {
299 return UITextContentTypeOrganizationName;
300 }
301
302 if ([hint isEqualToString:@"telephoneNumber"]) {
303 return UITextContentTypeTelephoneNumber;
304 }
305
306 if ([hint isEqualToString:@"password"]) {
307 return UITextContentTypePassword;
308 }
309
310 if ([hint isEqualToString:@"oneTimeCode"]) {
311 return UITextContentTypeOneTimeCode;
312 }
313
314 if ([hint isEqualToString:@"newPassword"]) {
315 return UITextContentTypeNewPassword;
316 }
317
318 return hints[0];
319 }
320
321 // Retrieves the autofillId from an input field's configuration. Returns
322 // nil if the field is nil and the input field is not a password field.
323 static NSString* AutofillIdFromDictionary(NSDictionary* dictionary) {
324 NSDictionary* autofill = dictionary[kAutofillProperties];
325 if (autofill) {
326 return autofill[kAutofillId];
327 }
328
329 // When autofill is nil, the field may still need an autofill id
330 // if the field is for password.
331 return [dictionary[kSecureTextEntry] boolValue] ? @"password" : nil;
332 }
333
334 // # Autofill Implementation Notes:
335 //
336 // Currently there're 2 types of autofills on iOS:
337 // - Regular autofill, including contact information and one-time-code,
338 // takes place in the form of predictive text in the quick type bar.
339 // This type of autofill does not save user input, and the keyboard
340 // currently only populates the focused field when a predictive text entry
341 // is selected by the user.
342 //
343 // - Password autofill, includes automatic strong password and regular
344 // password autofill. The former happens automatically when a
345 // "new password" field is detected and focused, and only that password
346 // field will be populated. The latter appears in the quick type bar when
347 // an eligible input field (which either has a UITextContentTypePassword
348 // contentType, or is a secure text entry) becomes the first responder, and may
349 // fill both the username and the password fields. iOS will attempt
350 // to save user input for both kinds of password fields. It's relatively
351 // tricky to deal with password autofill since it can autofill more than one
352 // field at a time and may employ heuristics based on what other text fields
353 // are in the same view controller.
354 //
355 // When a flutter text field is focused, and autofill is not explicitly disabled
356 // for it ("autofillable"), the framework collects its attributes and checks if
357 // it's in an AutofillGroup, and collects the attributes of other autofillable
358 // text fields in the same AutofillGroup if so. The attributes are sent to the
359 // text input plugin via a "TextInput.setClient" platform channel message. If
360 // autofill is disabled for a text field, its "autofill" field will be nil in
361 // the configuration json.
362 //
363 // The text input plugin then tries to determine which kind of autofill the text
364 // field needs. If the AutofillGroup the text field belongs to contains an
365 // autofillable text field that's password related, this text 's autofill type
366 // will be kFlutterAutofillTypePassword. If autofill is disabled for a text field,
367 // then its type will be kFlutterAutofillTypeNone. Otherwise the text field will
368 // have an autofill type of kFlutterAutofillTypeRegular.
369 //
370 // The text input plugin creates a new UIView for every kFlutterAutofillTypeNone
371 // text field. The UIView instance is never reused for other flutter text fields
372 // since the software keyboard often uses the identity of a UIView to distinguish
373 // different views and provides the same predictive text suggestions or restore
374 // the composing region if a UIView is reused for a different flutter text field.
375 //
376 // The text input plugin creates a new "autofill context" if the text field has
377 // the type of kFlutterAutofillTypePassword, to represent the AutofillGroup of
378 // the text field, and creates one FlutterTextInputView for every text field in
379 // the AutofillGroup.
380 //
381 // The text input plugin will try to reuse a UIView if a flutter text field's
382 // type is kFlutterAutofillTypeRegular, and has the same autofill id.
383 typedef NS_ENUM(NSInteger, FlutterAutofillType) {
384 // The field does not have autofillable content. Additionally if
385 // the field is currently in the autofill context, it will be
386 // removed from the context without triggering autofill save.
387 kFlutterAutofillTypeNone,
388 kFlutterAutofillTypeRegular,
389 kFlutterAutofillTypePassword,
390 };
391
392 static BOOL IsFieldPasswordRelated(NSDictionary* configuration) {
393 // Autofill is explicitly disabled if the id isn't present.
394 if (!AutofillIdFromDictionary(configuration)) {
395 return NO;
396 }
397
398 BOOL isSecureTextEntry = [configuration[kSecureTextEntry] boolValue];
399 if (isSecureTextEntry) {
400 return YES;
401 }
402
403 NSDictionary* autofill = configuration[kAutofillProperties];
404 UITextContentType contentType = ToUITextContentType(autofill[kAutofillHints]);
405
406 if ([contentType isEqualToString:UITextContentTypePassword] ||
407 [contentType isEqualToString:UITextContentTypeUsername]) {
408 return YES;
409 }
410
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 _selectedTextRange = [[FlutterTextRange alloc] initWithNSRange:NSMakeRange(0, 0)];
832 _markedRect = kInvalidFirstRect;
833 _cachedFirstRect = kInvalidFirstRect;
834 _scribbleInteractionStatus = FlutterScribbleInteractionStatusNone;
835 _pendingDeltas = [[NSMutableArray alloc] init];
836 // Initialize with the zero matrix which is not
837 // an affine transform.
838 _editableTransform = CATransform3D();
839
840 // UITextInputTraits
841 _autocapitalizationType = UITextAutocapitalizationTypeSentences;
842 _autocorrectionType = UITextAutocorrectionTypeDefault;
843 _spellCheckingType = UITextSpellCheckingTypeDefault;
844 _enablesReturnKeyAutomatically = NO;
845 _keyboardAppearance = UIKeyboardAppearanceDefault;
846 _keyboardType = UIKeyboardTypeDefault;
847 _returnKeyType = UIReturnKeyDone;
848 _secureTextEntry = NO;
849 _enableDeltaModel = NO;
850 _enableInteractiveSelection = YES;
851 _accessibilityEnabled = NO;
852 _smartQuotesType = UITextSmartQuotesTypeYes;
853 _smartDashesType = UITextSmartDashesTypeYes;
854 _selectionRects = [[NSArray alloc] init];
855
856 if (@available(iOS 14.0, *)) {
857 UIScribbleInteraction* interaction = [[UIScribbleInteraction alloc] initWithDelegate:self];
858 [self addInteraction:interaction];
859 }
860 }
861
862 return self;
863 }
864
865 - (void)configureWithDictionary:(NSDictionary*)configuration {
866 NSDictionary* inputType = configuration[kKeyboardType];
867 NSString* keyboardAppearance = configuration[kKeyboardAppearance];
868 NSDictionary* autofill = configuration[kAutofillProperties];
869
870 self.secureTextEntry = [configuration[kSecureTextEntry] boolValue];
871 self.enableDeltaModel = [configuration[kEnableDeltaModel] boolValue];
872
873 _isSystemKeyboardEnabled = ShouldShowSystemKeyboard(inputType);
874 self.keyboardType = ToUIKeyboardType(inputType);
875 self.returnKeyType = ToUIReturnKeyType(configuration[kInputAction]);
876 self.autocapitalizationType = ToUITextAutoCapitalizationType(configuration);
877 _enableInteractiveSelection = [configuration[kEnableInteractiveSelection] boolValue];
878 NSString* smartDashesType = configuration[kSmartDashesType];
879 // This index comes from the SmartDashesType enum in the framework.
880 bool smartDashesIsDisabled = smartDashesType && [smartDashesType isEqualToString:@"0"];
881 self.smartDashesType = smartDashesIsDisabled ? UITextSmartDashesTypeNo : UITextSmartDashesTypeYes;
882 NSString* smartQuotesType = configuration[kSmartQuotesType];
883 // This index comes from the SmartQuotesType enum in the framework.
884 bool smartQuotesIsDisabled = smartQuotesType && [smartQuotesType isEqualToString:@"0"];
885 self.smartQuotesType = smartQuotesIsDisabled ? UITextSmartQuotesTypeNo : UITextSmartQuotesTypeYes;
886 if ([keyboardAppearance isEqualToString:@"Brightness.dark"]) {
887 self.keyboardAppearance = UIKeyboardAppearanceDark;
888 } else if ([keyboardAppearance isEqualToString:@"Brightness.light"]) {
889 self.keyboardAppearance = UIKeyboardAppearanceLight;
890 } else {
891 self.keyboardAppearance = UIKeyboardAppearanceDefault;
892 }
893 NSString* autocorrect = configuration[kAutocorrectionType];
894 bool autocorrectIsDisabled = autocorrect && ![autocorrect boolValue];
895 self.autocorrectionType =
896 autocorrectIsDisabled ? UITextAutocorrectionTypeNo : UITextAutocorrectionTypeDefault;
897 self.spellCheckingType =
898 autocorrectIsDisabled ? UITextSpellCheckingTypeNo : UITextSpellCheckingTypeDefault;
899 self.autofillId = AutofillIdFromDictionary(configuration);
900 if (autofill == nil) {
901 self.textContentType = @"";
902 } else {
903 self.textContentType = ToUITextContentType(autofill[kAutofillHints]);
904 [self setTextInputState:autofill[kAutofillEditingValue]];
905 NSAssert(_autofillId, @"The autofill configuration must contain an autofill id");
906 }
907 // The input field needs to be visible for the system autofill
908 // to find it.
909 self.isVisibleToAutofill = autofill || _secureTextEntry;
910 }
911
912 - (UITextContentType)textContentType {
913 return _textContentType;
914 }
915
916 // Prevent UIKit from showing selection handles or highlights. This is needed
917 // because Scribble interactions require the view to have it's actual frame on
918 // the screen. They're not needed on iOS 17 with the new
919 // UITextSelectionDisplayInteraction API.
920 //
921 // These are undocumented methods. On iOS 17, the insertion point color is also
922 // used as the highlighted background of the selected IME candidate:
923 // https://github.com/flutter/flutter/issues/132548
924 // So the respondsToSelector method is overridden to return NO for this method
925 // on iOS 17+.
926 - (UIColor*)insertionPointColor {
927 return [UIColor clearColor];
928 }
929
930 - (UIColor*)selectionBarColor {
931 return [UIColor clearColor];
932 }
933
934 - (UIColor*)selectionHighlightColor {
935 return [UIColor clearColor];
936 }
937
938 - (UIInputViewController*)inputViewController {
939 if (_isSystemKeyboardEnabled) {
940 return nil;
941 }
942
943 if (!_inputViewController) {
944 _inputViewController = [[UIInputViewController alloc] init];
945 }
946 return _inputViewController;
947 }
948
949 - (id<FlutterTextInputDelegate>)textInputDelegate {
950 return _textInputPlugin.textInputDelegate;
951 }
952
953 - (BOOL)respondsToSelector:(SEL)selector {
954 if (@available(iOS 17.0, *)) {
955 // See the comment on this method.
956 if (selector == @selector(insertionPointColor)) {
957 return NO;
958 }
959 }
960 return [super respondsToSelector:selector];
961 }
962
963 - (void)setTextInputClient:(int)client {
964 _textInputClient = client;
965 _hasPlaceholder = NO;
966 }
967
968 - (UITextInteraction*)textInteraction API_AVAILABLE(ios(13.0)) {
969 if (!_textInteraction) {
970 _textInteraction = [UITextInteraction textInteractionForMode:UITextInteractionModeEditable];
971 _textInteraction.textInput = self;
972 }
973 return _textInteraction;
974 }
975
976 - (void)setTextInputState:(NSDictionary*)state {
977 if (@available(iOS 13.0, *)) {
978 // [UITextInteraction willMoveToView:] sometimes sets the textInput's inputDelegate
979 // to nil. This is likely a bug in UIKit. In order to inform the keyboard of text
980 // and selection changes when that happens, add a dummy UITextInteraction to this
981 // view so it sets a valid inputDelegate that we can call textWillChange et al. on.
982 // See https://github.com/flutter/engine/pull/32881.
983 if (!self.inputDelegate && self.isFirstResponder) {
984 [self addInteraction:self.textInteraction];
985 }
986 }
987
988 NSString* newText = state[@"text"];
989 BOOL textChanged = ![self.text isEqualToString:newText];
990 if (textChanged) {
991 [self.inputDelegate textWillChange:self];
992 [self.text setString:newText];
993 }
994 NSInteger composingBase = [state[@"composingBase"] intValue];
995 NSInteger composingExtent = [state[@"composingExtent"] intValue];
996 NSRange composingRange = [self clampSelection:NSMakeRange(MIN(composingBase, composingExtent),
997 ABS(composingBase - composingExtent))
998 forText:self.text];
999
1000 self.markedTextRange =
1001 composingRange.length > 0 ? [FlutterTextRange rangeWithNSRange:composingRange] : nil;
1002
1003 NSRange selectedRange = [self clampSelectionFromBase:[state[@"selectionBase"] intValue]
1004 extent:[state[@"selectionExtent"] intValue]
1005 forText:self.text];
1006
1007 NSRange oldSelectedRange = [(FlutterTextRange*)self.selectedTextRange range];
1008 if (!NSEqualRanges(selectedRange, oldSelectedRange)) {
1009 [self.inputDelegate selectionWillChange:self];
1010
1011 [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]];
1012
1013 _selectionAffinity = kTextAffinityDownstream;
1014 if ([state[@"selectionAffinity"] isEqualToString:@(kTextAffinityUpstream)]) {
1015 _selectionAffinity = kTextAffinityUpstream;
1016 }
1017 [self.inputDelegate selectionDidChange:self];
1018 }
1019
1020 if (textChanged) {
1021 [self.inputDelegate textDidChange:self];
1022 }
1023
1024 if (@available(iOS 13.0, *)) {
1025 if (_textInteraction) {
1026 [self removeInteraction:_textInteraction];
1027 }
1028 }
1029 }
1030
1031 // Forward touches to the viewResponder to allow tapping inside the UITextField as normal.
1032 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
1033 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1034 [self resetScribbleInteractionStatusIfEnding];
1035 [self.viewResponder touchesBegan:touches withEvent:event];
1036 }
1037
1038 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
1039 [self.viewResponder touchesMoved:touches withEvent:event];
1040 }
1041
1042 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
1043 [self.viewResponder touchesEnded:touches withEvent:event];
1044 }
1045
1046 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1047 [self.viewResponder touchesCancelled:touches withEvent:event];
1048 }
1049
1050 - (void)touchesEstimatedPropertiesUpdated:(NSSet*)touches {
1051 [self.viewResponder touchesEstimatedPropertiesUpdated:touches];
1052 }
1053
1054 // Extracts the selection information from the editing state dictionary.
1055 //
1056 // The state may contain an invalid selection, such as when no selection was
1057 // explicitly set in the framework. This is handled here by setting the
1058 // selection to (0,0). In contrast, Android handles this situation by
1059 // clearing the selection, but the result in both cases is that the cursor
1060 // is placed at the beginning of the field.
1061 - (NSRange)clampSelectionFromBase:(int)selectionBase
1062 extent:(int)selectionExtent
1063 forText:(NSString*)text {
1064 int loc = MIN(selectionBase, selectionExtent);
1065 int len = ABS(selectionExtent - selectionBase);
1066 return loc < 0 ? NSMakeRange(0, 0)
1067 : [self clampSelection:NSMakeRange(loc, len) forText:self.text];
1068 }
1069
1070 - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text {
1071 NSUInteger start = MIN(MAX(range.location, 0), text.length);
1072 NSUInteger length = MIN(range.length, text.length - start);
1073 return NSMakeRange(start, length);
1074 }
1075
1076 - (BOOL)isVisibleToAutofill {
1077 return self.frame.size.width > 0 && self.frame.size.height > 0;
1078 }
1079
1080 // An input view is generally ignored by password autofill attempts, if it's
1081 // not the first responder and is zero-sized. For input fields that are in the
1082 // autofill context but do not belong to the current autofill group, setting
1083 // their frames to CGRectZero prevents ios autofill from taking them into
1084 // account.
1085 - (void)setIsVisibleToAutofill:(BOOL)isVisibleToAutofill {
1086 // This probably needs to change (think it is getting overwritten by the updateSizeAndTransform
1087 // stuff for now).
1088 self.frame = isVisibleToAutofill ? CGRectMake(0, 0, 1, 1) : CGRectZero;
1089 }
1090
1091 #pragma mark UIScribbleInteractionDelegate
1092
1093 // Checks whether Scribble features are possibly available – meaning this is an iPad running iOS
1094 // 14 or higher.
1095 - (BOOL)isScribbleAvailable {
1096 if (@available(iOS 14.0, *)) {
1097 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
1098 return YES;
1099 }
1100 }
1101 return NO;
1102 }
1103
1104 - (void)scribbleInteractionWillBeginWriting:(UIScribbleInteraction*)interaction
1105 API_AVAILABLE(ios(14.0)) {
1106 _scribbleInteractionStatus = FlutterScribbleInteractionStatusStarted;
1107 [self.textInputDelegate flutterTextInputViewScribbleInteractionBegan:self];
1108 }
1109
1110 - (void)scribbleInteractionDidFinishWriting:(UIScribbleInteraction*)interaction
1111 API_AVAILABLE(ios(14.0)) {
1112 _scribbleInteractionStatus = FlutterScribbleInteractionStatusEnding;
1113 [self.textInputDelegate flutterTextInputViewScribbleInteractionFinished:self];
1114 }
1115
1116 - (BOOL)scribbleInteraction:(UIScribbleInteraction*)interaction
1117 shouldBeginAtLocation:(CGPoint)location API_AVAILABLE(ios(14.0)) {
1118 return YES;
1119 }
1120
1121 - (BOOL)scribbleInteractionShouldDelayFocus:(UIScribbleInteraction*)interaction
1122 API_AVAILABLE(ios(14.0)) {
1123 return NO;
1124 }
1125
1126 #pragma mark - UIResponder Overrides
1127
1128 - (BOOL)canBecomeFirstResponder {
1129 // Only the currently focused input field can
1130 // become the first responder. This prevents iOS
1131 // from changing focus by itself (the framework
1132 // focus will be out of sync if that happens).
1133 return _textInputClient != 0;
1134 }
1135
1136 - (BOOL)resignFirstResponder {
1137 BOOL success = [super resignFirstResponder];
1138 if (success) {
1139 if (!_preventCursorDismissWhenResignFirstResponder) {
1140 [self.textInputDelegate flutterTextInputView:self
1141 didResignFirstResponderWithTextInputClient:_textInputClient];
1142 }
1143 }
1144 return success;
1145 }
1146
1147 - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
1148 if (action == @selector(paste:)) {
1149 // Forbid pasting images, memojis, or other non-string content.
1150 return [UIPasteboard generalPasteboard].hasStrings;
1151 }
1152
1153 return [super canPerformAction:action withSender:sender];
1154 }
1155
1156 #pragma mark - UIResponderStandardEditActions Overrides
1157
1158 - (void)cut:(id)sender {
1159 [UIPasteboard generalPasteboard].string = [self textInRange:_selectedTextRange];
1160 [self replaceRange:_selectedTextRange withText:@""];
1161 }
1162
1163 - (void)copy:(id)sender {
1164 [UIPasteboard generalPasteboard].string = [self textInRange:_selectedTextRange];
1165 }
1166
1167 - (void)paste:(id)sender {
1168 NSString* pasteboardString = [UIPasteboard generalPasteboard].string;
1169 if (pasteboardString != nil) {
1170 [self insertText:pasteboardString];
1171 }
1172 }
1173
1174 - (void)delete:(id)sender {
1175 [self replaceRange:_selectedTextRange withText:@""];
1176 }
1177
1178 - (void)selectAll:(id)sender {
1179 [self setSelectedTextRange:[self textRangeFromPosition:[self beginningOfDocument]
1180 toPosition:[self endOfDocument]]];
1181 }
1182
1183 #pragma mark - UITextInput Overrides
1184
1185 - (id<UITextInputTokenizer>)tokenizer {
1186 if (_tokenizer == nil) {
1187 _tokenizer = [[FlutterTokenizer alloc] initWithTextInput:self];
1188 }
1189 return _tokenizer;
1190 }
1191
1192 - (UITextRange*)selectedTextRange {
1193 return [_selectedTextRange copy];
1194 }
1195
1196 // Change the range of selected text, without notifying the framework.
1197 - (void)setSelectedTextRangeLocal:(UITextRange*)selectedTextRange {
1198 if (_selectedTextRange != selectedTextRange) {
1199 if (self.hasText) {
1200 FlutterTextRange* flutterTextRange = (FlutterTextRange*)selectedTextRange;
1201 _selectedTextRange = [[FlutterTextRange
1202 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, flutterTextRange.range)] copy];
1203 } else {
1204 _selectedTextRange = [selectedTextRange copy];
1205 }
1206 }
1207 }
1208
1209 - (void)setSelectedTextRange:(UITextRange*)selectedTextRange {
1210 if (!_enableInteractiveSelection) {
1211 return;
1212 }
1213
1214 [self setSelectedTextRangeLocal:selectedTextRange];
1215
1216 if (_enableDeltaModel) {
1217 [self updateEditingStateWithDelta:flutter::TextEditingDelta([self.text UTF8String])];
1218 } else {
1219 [self updateEditingState];
1220 }
1221
1222 if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone ||
1223 _scribbleFocusStatus == FlutterScribbleFocusStatusFocused) {
1224 NSAssert([selectedTextRange isKindOfClass:[FlutterTextRange class]],
1225 @"Expected a FlutterTextRange for range (got %@).", [selectedTextRange class]);
1226 FlutterTextRange* flutterTextRange = (FlutterTextRange*)selectedTextRange;
1227 if (flutterTextRange.range.length > 0) {
1228 [self.textInputDelegate flutterTextInputView:self showToolbar:_textInputClient];
1229 }
1230 }
1231
1232 [self resetScribbleInteractionStatusIfEnding];
1233 }
1234
1235 - (id)insertDictationResultPlaceholder {
1236 return @"";
1237 }
1238
1239 - (void)removeDictationResultPlaceholder:(id)placeholder willInsertResult:(BOOL)willInsertResult {
1240 }
1241
1242 - (NSString*)textInRange:(UITextRange*)range {
1243 if (!range) {
1244 return nil;
1245 }
1246 NSAssert([range isKindOfClass:[FlutterTextRange class]],
1247 @"Expected a FlutterTextRange for range (got %@).", [range class]);
1248 NSRange textRange = ((FlutterTextRange*)range).range;
1249 NSAssert(textRange.location != NSNotFound, @"Expected a valid text range.");
1250 // Sanitize the range to prevent going out of bounds.
1251 NSUInteger location = MIN(textRange.location, self.text.length);
1252 NSUInteger length = MIN(self.text.length - location, textRange.length);
1253 NSRange safeRange = NSMakeRange(location, length);
1254 return [self.text substringWithRange:safeRange];
1255 }
1256
1257 // Replace the text within the specified range with the given text,
1258 // without notifying the framework.
1259 - (void)replaceRangeLocal:(NSRange)range withText:(NSString*)text {
1260 [self.text replaceCharactersInRange:[self clampSelection:range forText:self.text]
1261 withString:text];
1262
1263 // Adjust the selected range and the marked text range. There's no
1264 // documentation but UITextField always sets markedTextRange to nil,
1265 // and collapses the selection to the end of the new replacement text.
1266 const NSRange newSelectionRange =
1267 [self clampSelection:NSMakeRange(range.location + text.length, 0) forText:self.text];
1268
1269 [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:newSelectionRange]];
1270 self.markedTextRange = nil;
1271 }
1272
1273 - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
1274 NSString* textBeforeChange = [self.text copy];
1275 NSRange replaceRange = ((FlutterTextRange*)range).range;
1276 [self replaceRangeLocal:replaceRange withText:text];
1277 if (_enableDeltaModel) {
1278 NSRange nextReplaceRange = [self clampSelection:replaceRange forText:textBeforeChange];
1279 [self updateEditingStateWithDelta:flutter::TextEditingDelta(
1280 [textBeforeChange UTF8String],
1281 flutter::TextRange(
1282 nextReplaceRange.location,
1283 nextReplaceRange.location + nextReplaceRange.length),
1284 [text UTF8String])];
1285 } else {
1286 [self updateEditingState];
1287 }
1288 }
1289
1290 - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
1291 // `temporarilyDeletedComposedCharacter` should only be used during a single text change session.
1292 // So it needs to be cleared at the start of each text editing session.
1293 self.temporarilyDeletedComposedCharacter = nil;
1294
1295 if (self.returnKeyType == UIReturnKeyDefault && [text isEqualToString:@"\n"]) {
1296 [self.textInputDelegate flutterTextInputView:self
1297 performAction:FlutterTextInputActionNewline
1298 withClient:_textInputClient];
1299 return YES;
1300 }
1301
1302 if ([text isEqualToString:@"\n"]) {
1303 FlutterTextInputAction action;
1304 switch (self.returnKeyType) {
1305 case UIReturnKeyDefault:
1306 action = FlutterTextInputActionUnspecified;
1307 break;
1308 case UIReturnKeyDone:
1309 action = FlutterTextInputActionDone;
1310 break;
1311 case UIReturnKeyGo:
1312 action = FlutterTextInputActionGo;
1313 break;
1314 case UIReturnKeySend:
1315 action = FlutterTextInputActionSend;
1316 break;
1317 case UIReturnKeySearch:
1318 case UIReturnKeyGoogle:
1319 case UIReturnKeyYahoo:
1320 action = FlutterTextInputActionSearch;
1321 break;
1322 case UIReturnKeyNext:
1323 action = FlutterTextInputActionNext;
1324 break;
1325 case UIReturnKeyContinue:
1326 action = FlutterTextInputActionContinue;
1327 break;
1328 case UIReturnKeyJoin:
1329 action = FlutterTextInputActionJoin;
1330 break;
1331 case UIReturnKeyRoute:
1332 action = FlutterTextInputActionRoute;
1333 break;
1334 case UIReturnKeyEmergencyCall:
1335 action = FlutterTextInputActionEmergencyCall;
1336 break;
1337 }
1338
1339 [self.textInputDelegate flutterTextInputView:self
1340 performAction:action
1341 withClient:_textInputClient];
1342 return NO;
1343 }
1344
1345 return YES;
1346 }
1347
1348 // Either replaces the existing marked text or, if none is present, inserts it in
1349 // place of the current selection.
1350 - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelectedRange {
1351 NSString* textBeforeChange = [self.text copy];
1352
1353 if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone ||
1354 _scribbleFocusStatus != FlutterScribbleFocusStatusUnfocused) {
1355 return;
1356 }
1357
1358 if (markedText == nil) {
1359 markedText = @"";
1360 }
1361
1362 const FlutterTextRange* currentMarkedTextRange = (FlutterTextRange*)self.markedTextRange;
1363 const NSRange& actualReplacedRange = currentMarkedTextRange && !currentMarkedTextRange.isEmpty
1364 ? currentMarkedTextRange.range
1365 : _selectedTextRange.range;
1366 // No need to call replaceRangeLocal as this method always adjusts the
1367 // selected/marked text ranges anyways.
1368 [self.text replaceCharactersInRange:actualReplacedRange withString:markedText];
1369
1370 const NSRange newMarkedRange = NSMakeRange(actualReplacedRange.location, markedText.length);
1371 self.markedTextRange =
1372 newMarkedRange.length > 0 ? [FlutterTextRange rangeWithNSRange:newMarkedRange] : nil;
1373
1374 [self setSelectedTextRangeLocal:
1375 [FlutterTextRange
1376 rangeWithNSRange:[self clampSelection:NSMakeRange(markedSelectedRange.location +
1377 newMarkedRange.location,
1378 markedSelectedRange.length)
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 // The iOS 16 system highlight does not repect the height returned by `firstRectForRange`
1689 // API (unlike iOS 17). So we return CGRectZero to hide it (unless if scribble is enabled).
1690 // To support scribble's advanced gestures (e.g. insert a space with a vertical bar),
1691 // at least 1 character's width is required.
1692 if (@available(iOS 17, *)) {
1693 // No-op
1694 } else if (![self isScribbleAvailable]) {
1695 return CGRectZero;
1696 }
1697
1698 NSUInteger first = start;
1699 if (end < start) {
1700 first = end;
1701 }
1702
1703 CGRect startSelectionRect = CGRectNull;
1704 CGRect endSelectionRect = CGRectNull;
1705 // Selection rects from different langauges may have different minY/maxY.
1706 // So we need to iterate through each rects to update minY/maxY.
1707 CGFloat minY = CGFLOAT_MAX;
1708 CGFloat maxY = CGFLOAT_MIN;
1709
1710 FlutterTextRange* textRange = [FlutterTextRange
1711 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1712 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1713 BOOL startsOnOrBeforeStartOfRange = _selectionRects[i].position <= first;
1714 BOOL isLastSelectionRect = i + 1 == [_selectionRects count];
1715 BOOL endOfTextIsAfterStartOfRange = isLastSelectionRect && textRange.range.length > first;
1716 BOOL nextSelectionRectIsAfterStartOfRange =
1717 !isLastSelectionRect && _selectionRects[i + 1].position > first;
1718 if (startsOnOrBeforeStartOfRange &&
1719 (endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
1720 // TODO(hellohaunlin): Remove iOS 17 check. The logic should also work for older versions.
1721 if (@available(iOS 17, *)) {
1722 startSelectionRect = _selectionRects[i].rect;
1723 } else {
1724 return _selectionRects[i].rect;
1725 }
1726 }
1727 if (!CGRectIsNull(startSelectionRect)) {
1728 minY = fmin(minY, CGRectGetMinY(_selectionRects[i].rect));
1729 maxY = fmax(maxY, CGRectGetMaxY(_selectionRects[i].rect));
1730 BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1; // end is exclusive
1731 BOOL nextSelectionRectIsOnNextLine =
1732 !isLastSelectionRect &&
1733 // Selection rects from different langauges in 2 lines may overlap with each other.
1734 // A good approximation is to check if the center of next rect is below the bottom of
1735 // current rect.
1736 // TODO(hellohuanlin): Consider passing the line break info from framework.
1737 CGRectGetMidY(_selectionRects[i + 1].rect) > CGRectGetMaxY(_selectionRects[i].rect);
1738 if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectionRectIsOnNextLine) {
1739 endSelectionRect = _selectionRects[i].rect;
1740 break;
1741 }
1742 }
1743 }
1744 if (CGRectIsNull(startSelectionRect) || CGRectIsNull(endSelectionRect)) {
1745 return CGRectZero;
1746 } else {
1747 // fmin/fmax to support both LTR and RTL languages.
1748 CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
1749 CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
1750 return CGRectMake(minX, minY, maxX - minX, maxY - minY);
1751 }
1752 }
1753
1754 - (CGRect)caretRectForPosition:(UITextPosition*)position {
1755 NSInteger index = ((FlutterTextPosition*)position).index;
1756 UITextStorageDirection affinity = ((FlutterTextPosition*)position).affinity;
1757 // Get the selectionRect of the characters before and after the requested caret position.
1758 NSArray<UITextSelectionRect*>* rects = [self
1759 selectionRectsForRange:[FlutterTextRange
1760 rangeWithNSRange:fml::RangeForCharactersInRange(
1761 self.text,
1762 NSMakeRange(
1763 MAX(0, index - 1),
1764 (index >= (NSInteger)self.text.length)
1765 ? 1
1766 : 2))]];
1767 if (rects.count == 0) {
1768 return CGRectZero;
1769 }
1770 if (index == 0) {
1771 // There is no character before the caret, so this will be the bounds of the character after the
1772 // caret position.
1773 CGRect characterAfterCaret = rects[0].rect;
1774 // Return a zero-width rectangle along the upstream edge of the character after the caret
1775 // position.
1776 if ([rects[0] isKindOfClass:[FlutterTextSelectionRect class]] &&
1777 ((FlutterTextSelectionRect*)rects[0]).isRTL) {
1778 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1779 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1780 } else {
1781 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1782 characterAfterCaret.size.height);
1783 }
1784 } else if (rects.count == 2 && affinity == UITextStorageDirectionForward) {
1785 // There are characters before and after the caret, with forward direction affinity.
1786 // It's better to use the character after the caret.
1787 CGRect characterAfterCaret = rects[1].rect;
1788 // Return a zero-width rectangle along the upstream edge of the character after the caret
1789 // position.
1790 if ([rects[1] isKindOfClass:[FlutterTextSelectionRect class]] &&
1791 ((FlutterTextSelectionRect*)rects[1]).isRTL) {
1792 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1793 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1794 } else {
1795 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1796 characterAfterCaret.size.height);
1797 }
1798 }
1799
1800 // Covers 2 remaining cases:
1801 // 1. there are characters before and after the caret, with backward direction affinity.
1802 // 2. there is only 1 character before the caret (caret is at the end of text).
1803 // For both cases, return a zero-width rectangle along the downstream edge of the character
1804 // before the caret position.
1805 CGRect characterBeforeCaret = rects[0].rect;
1806 if ([rects[0] isKindOfClass:[FlutterTextSelectionRect class]] &&
1807 ((FlutterTextSelectionRect*)rects[0]).isRTL) {
1808 return CGRectMake(characterBeforeCaret.origin.x, characterBeforeCaret.origin.y, 0,
1809 characterBeforeCaret.size.height);
1810 } else {
1811 return CGRectMake(characterBeforeCaret.origin.x + characterBeforeCaret.size.width,
1812 characterBeforeCaret.origin.y, 0, characterBeforeCaret.size.height);
1813 }
1814 }
1815
1816 - (UITextPosition*)closestPositionToPoint:(CGPoint)point {
1817 if ([_selectionRects count] == 0) {
1818 NSAssert([_selectedTextRange.start isKindOfClass:[FlutterTextPosition class]],
1819 @"Expected a FlutterTextPosition for position (got %@).",
1820 [_selectedTextRange.start class]);
1821 NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index;
1822 UITextStorageDirection currentAffinity =
1823 ((FlutterTextPosition*)_selectedTextRange.start).affinity;
1824 return [FlutterTextPosition positionWithIndex:currentIndex affinity:currentAffinity];
1825 }
1826
1827 FlutterTextRange* range = [FlutterTextRange
1828 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1829 return [self closestPositionToPoint:point withinRange:range];
1830 }
1831
1832 - (NSArray*)selectionRectsForRange:(UITextRange*)range {
1833 // At least in the simulator, swapping to the Japanese keyboard crashes the app as this method
1834 // is called immediately with a UITextRange with a UITextPosition rather than FlutterTextPosition
1835 // for the start and end.
1836 if (![range.start isKindOfClass:[FlutterTextPosition class]]) {
1837 return @[];
1838 }
1839 NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
1840 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
1841 NSAssert([range.end isKindOfClass:[FlutterTextPosition class]],
1842 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]);
1843 NSUInteger start = ((FlutterTextPosition*)range.start).index;
1844 NSUInteger end = ((FlutterTextPosition*)range.end).index;
1845 NSMutableArray* rects = [[NSMutableArray alloc] init];
1846 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1847 if (_selectionRects[i].position >= start &&
1848 (_selectionRects[i].position < end ||
1849 (start == end && _selectionRects[i].position <= end))) {
1850 float width = _selectionRects[i].rect.size.width;
1851 if (start == end) {
1852 width = 0;
1853 }
1854 CGRect rect = CGRectMake(_selectionRects[i].rect.origin.x, _selectionRects[i].rect.origin.y,
1855 width, _selectionRects[i].rect.size.height);
1856 FlutterTextSelectionRect* selectionRect = [FlutterTextSelectionRect
1857 selectionRectWithRectAndInfo:rect
1858 position:_selectionRects[i].position
1859 writingDirection:NSWritingDirectionNatural
1860 containsStart:(i == 0)
1861 containsEnd:(i == fml::RangeForCharactersInRange(
1862 self.text, NSMakeRange(0, self.text.length))
1863 .length)
1864 isVertical:NO];
1865 [rects addObject:selectionRect];
1866 }
1867 }
1868 return rects;
1869 }
1870
1871 - (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range {
1872 NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
1873 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
1874 NSAssert([range.end isKindOfClass:[FlutterTextPosition class]],
1875 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]);
1876 NSUInteger start = ((FlutterTextPosition*)range.start).index;
1877 NSUInteger end = ((FlutterTextPosition*)range.end).index;
1878
1879 // Selecting text using the floating cursor is not as precise as the pencil.
1880 // Allow further vertical deviation and base more of the decision on horizontal comparison.
1881 CGFloat verticalPrecision = _isFloatingCursorActive ? 10 : 1;
1882
1883 // Find the selectionRect with a leading-center point that is closest to a given point.
1884 BOOL isFirst = YES;
1885 NSUInteger _closestRectIndex = 0;
1886 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1887 NSUInteger position = _selectionRects[i].position;
1888 if (position >= start && position <= end) {
1889 if (isFirst ||
1890 IsSelectionRectBoundaryCloserToPoint(
1891 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
1892 /*useTrailingBoundaryOfSelectionRect=*/NO, _selectionRects[_closestRectIndex].rect,
1893 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
1894 isFirst = NO;
1895 _closestRectIndex = i;
1896 }
1897 }
1898 }
1899
1900 FlutterTextPosition* closestPosition =
1901 [FlutterTextPosition positionWithIndex:_selectionRects[_closestRectIndex].position
1902 affinity:UITextStorageDirectionForward];
1903
1904 // Check if the far side of the closest rect is a better fit (e.g. tapping end of line)
1905 // Cannot simply check the _closestRectIndex result from the previous for loop due to RTL
1906 // writing direction and the gaps between selectionRects. So we also need to consider
1907 // the adjacent selectionRects to refine _closestRectIndex.
1908 for (NSUInteger i = MAX(0, _closestRectIndex - 1);
1909 i < MIN(_closestRectIndex + 2, [_selectionRects count]); i++) {
1910 NSUInteger position = _selectionRects[i].position + 1;
1911 if (position >= start && position <= end) {
1912 if (IsSelectionRectBoundaryCloserToPoint(
1913 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
1914 /*useTrailingBoundaryOfSelectionRect=*/YES, _selectionRects[_closestRectIndex].rect,
1915 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
1916 // This is an upstream position
1917 closestPosition = [FlutterTextPosition positionWithIndex:position
1918 affinity:UITextStorageDirectionBackward];
1919 }
1920 }
1921 }
1922
1923 return closestPosition;
1924 }
1925
1926 - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
1927 // TODO(cbracken) Implement.
1928 NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index;
1929 return [FlutterTextRange rangeWithNSRange:fml::RangeForCharacterAtIndex(self.text, currentIndex)];
1930 }
1931
1932 // Overall logic for floating cursor's "move" gesture and "selection" gesture:
1933 //
1934 // Floating cursor's "move" gesture takes 1 finger to force press the space bar, and then move the
1935 // cursor. The process starts with `beginFloatingCursorAtPoint`. When the finger is moved,
1936 // `updateFloatingCursorAtPoint` will be called. When the finger is released, `endFloatingCursor`
1937 // will be called. In all cases, we send the point (relative to the initial point registered in
1938 // beginFloatingCursorAtPoint) to the framework, so that framework can animate the floating cursor.
1939 //
1940 // During the move gesture, the framework only animate the cursor visually. It's only
1941 // after the gesture is complete, will the framework update the selection to the cursor's
1942 // new position (with zero selection length). This means during the animation, the visual effect
1943 // of the cursor is temporarily out of sync with the selection state in both framework and engine.
1944 // But it will be in sync again after the animation is complete.
1945 //
1946 // Floating cursor's "selection" gesture also starts with 1 finger to force press the space bar,
1947 // so exactly the same functions as the "move gesture" discussed above will be called. When the
1948 // second finger is pressed, `setSelectedText` will be called. This mechanism requires
1949 // `closestPositionFromPoint` to be implemented, to allow UIKit to translate the finger touch
1950 // location displacement to the text range to select. When the selection is completed
1951 // (i.e. when both of the 2 fingers are released), similar to "move" gesture,
1952 // the `endFloatingCursor` will be called.
1953 //
1954 // When the 2nd finger is pressed, it does not trigger another startFloatingCursor call. So
1955 // floating cursor move/selection logic has to be implemented in iOS embedder rather than
1956 // just the framework side.
1957 //
1958 // Whenever a selection is updated, the engine sends the new selection to the framework. So unlike
1959 // the move gesture, the selections in the framework and the engine are always kept in sync.
1960 - (void)beginFloatingCursorAtPoint:(CGPoint)point {
1961 // For "beginFloatingCursorAtPoint" and "updateFloatingCursorAtPoint", "point" is roughly:
1962 //
1963 // CGPoint(
1964 // width >= 0 ? point.x.clamp(boundingBox.left, boundingBox.right) : point.x,
1965 // height >= 0 ? point.y.clamp(boundingBox.top, boundingBox.bottom) : point.y,
1966 // )
1967 // where
1968 // point = keyboardPanGestureRecognizer.translationInView(textInputView) + caretRectForPosition
1969 // boundingBox = self.convertRect(bounds, fromView:textInputView)
1970 // bounds = self._selectionClipRect ?? self.bounds
1971 //
1972 // It seems impossible to use a negative "width" or "height", as the "convertRect"
1973 // call always turns a CGRect's negative dimensions into non-negative values, e.g.,
1974 // (1, 2, -3, -4) would become (-2, -2, 3, 4).
1975 _isFloatingCursorActive = YES;
1976 _floatingCursorOffset = point;
1977 [self.textInputDelegate flutterTextInputView:self
1978 updateFloatingCursor:FlutterFloatingCursorDragStateStart
1979 withClient:_textInputClient
1980 withPosition:@{@"X" : @0, @"Y" : @0}];
1981 }
1982
1983 - (void)updateFloatingCursorAtPoint:(CGPoint)point {
1984 [self.textInputDelegate flutterTextInputView:self
1985 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
1986 withClient:_textInputClient
1987 withPosition:@{
1988 @"X" : @(point.x - _floatingCursorOffset.x),
1989 @"Y" : @(point.y - _floatingCursorOffset.y)
1990 }];
1991 }
1992
1993 - (void)endFloatingCursor {
1994 _isFloatingCursorActive = NO;
1995 [self.textInputDelegate flutterTextInputView:self
1996 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
1997 withClient:_textInputClient
1998 withPosition:@{@"X" : @0, @"Y" : @0}];
1999 }
2000
2001 #pragma mark - UIKeyInput Overrides
2002
2003 - (void)updateEditingState {
2004 NSUInteger selectionBase = ((FlutterTextPosition*)_selectedTextRange.start).index;
2005 NSUInteger selectionExtent = ((FlutterTextPosition*)_selectedTextRange.end).index;
2006
2007 // Empty compositing range is represented by the framework's TextRange.empty.
2008 NSInteger composingBase = -1;
2009 NSInteger composingExtent = -1;
2010 if (self.markedTextRange != nil) {
2011 composingBase = ((FlutterTextPosition*)self.markedTextRange.start).index;
2012 composingExtent = ((FlutterTextPosition*)self.markedTextRange.end).index;
2013 }
2014 NSDictionary* state = @{
2015 @"selectionBase" : @(selectionBase),
2016 @"selectionExtent" : @(selectionExtent),
2017 @"selectionAffinity" : @(_selectionAffinity),
2018 @"selectionIsDirectional" : @(false),
2019 @"composingBase" : @(composingBase),
2020 @"composingExtent" : @(composingExtent),
2021 @"text" : [NSString stringWithString:self.text],
2022 };
2023
2024 if (_textInputClient == 0 && _autofillId != nil) {
2025 [self.textInputDelegate flutterTextInputView:self
2026 updateEditingClient:_textInputClient
2027 withState:state
2028 withTag:_autofillId];
2029 } else {
2030 [self.textInputDelegate flutterTextInputView:self
2031 updateEditingClient:_textInputClient
2032 withState:state];
2033 }
2034 }
2035
2036 - (void)updateEditingStateWithDelta:(flutter::TextEditingDelta)delta {
2037 NSUInteger selectionBase = ((FlutterTextPosition*)_selectedTextRange.start).index;
2038 NSUInteger selectionExtent = ((FlutterTextPosition*)_selectedTextRange.end).index;
2039
2040 // Empty compositing range is represented by the framework's TextRange.empty.
2041 NSInteger composingBase = -1;
2042 NSInteger composingExtent = -1;
2043 if (self.markedTextRange != nil) {
2044 composingBase = ((FlutterTextPosition*)self.markedTextRange.start).index;
2045 composingExtent = ((FlutterTextPosition*)self.markedTextRange.end).index;
2046 }
2047
2048 NSDictionary* deltaToFramework = @{
2049 @"oldText" : @(delta.old_text().c_str()),
2050 @"deltaText" : @(delta.delta_text().c_str()),
2051 @"deltaStart" : @(delta.delta_start()),
2052 @"deltaEnd" : @(delta.delta_end()),
2053 @"selectionBase" : @(selectionBase),
2054 @"selectionExtent" : @(selectionExtent),
2055 @"selectionAffinity" : @(_selectionAffinity),
2056 @"selectionIsDirectional" : @(false),
2057 @"composingBase" : @(composingBase),
2058 @"composingExtent" : @(composingExtent),
2059 };
2060
2061 [_pendingDeltas addObject:deltaToFramework];
2062
2063 if (_pendingDeltas.count == 1) {
2064 __weak FlutterTextInputView* weakSelf = self;
2065 dispatch_async(dispatch_get_main_queue(), ^{
2066 __strong FlutterTextInputView* strongSelf = weakSelf;
2067 if (strongSelf && strongSelf.pendingDeltas.count > 0) {
2068 NSDictionary* deltas = @{
2069 @"deltas" : strongSelf.pendingDeltas,
2070 };
2071
2072 [strongSelf.textInputDelegate flutterTextInputView:strongSelf
2073 updateEditingClient:strongSelf->_textInputClient
2074 withDelta:deltas];
2075 [strongSelf.pendingDeltas removeAllObjects];
2076 }
2077 });
2078 }
2079 }
2080
2081 - (BOOL)hasText {
2082 return self.text.length > 0;
2083 }
2084
2085 - (void)insertText:(NSString*)text {
2086 if (self.temporarilyDeletedComposedCharacter.length > 0 && text.length == 1 && !text.UTF8String &&
2087 [text characterAtIndex:0] == [self.temporarilyDeletedComposedCharacter characterAtIndex:0]) {
2088 // Workaround for https://github.com/flutter/flutter/issues/111494
2089 // TODO(cyanglaz): revert this workaround if when flutter supports a minimum iOS version which
2090 // this bug is fixed by Apple.
2091 text = self.temporarilyDeletedComposedCharacter;
2092 self.temporarilyDeletedComposedCharacter = nil;
2093 }
2094
2095 NSMutableArray<FlutterTextSelectionRect*>* copiedRects =
2096 [[NSMutableArray alloc] initWithCapacity:[_selectionRects count]];
2097 NSAssert([_selectedTextRange.start isKindOfClass:[FlutterTextPosition class]],
2098 @"Expected a FlutterTextPosition for position (got %@).",
2099 [_selectedTextRange.start class]);
2100 NSUInteger insertPosition = ((FlutterTextPosition*)_selectedTextRange.start).index;
2101 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2102 NSUInteger rectPosition = _selectionRects[i].position;
2103 if (rectPosition == insertPosition) {
2104 for (NSUInteger j = 0; j <= text.length; j++) {
2105 [copiedRects addObject:[FlutterTextSelectionRect
2106 selectionRectWithRect:_selectionRects[i].rect
2107 position:rectPosition + j
2108 writingDirection:_selectionRects[i].writingDirection]];
2109 }
2110 } else {
2111 if (rectPosition > insertPosition) {
2112 rectPosition = rectPosition + text.length;
2113 }
2114 [copiedRects addObject:[FlutterTextSelectionRect
2115 selectionRectWithRect:_selectionRects[i].rect
2116 position:rectPosition
2117 writingDirection:_selectionRects[i].writingDirection]];
2118 }
2119 }
2120
2121 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2122 [self resetScribbleInteractionStatusIfEnding];
2123 self.selectionRects = copiedRects;
2124 _selectionAffinity = kTextAffinityDownstream;
2125 [self replaceRange:_selectedTextRange withText:text];
2126 }
2127
2128 - (UITextPlaceholder*)insertTextPlaceholderWithSize:(CGSize)size API_AVAILABLE(ios(13.0)) {
2129 [self.textInputDelegate flutterTextInputView:self
2130 insertTextPlaceholderWithSize:size
2131 withClient:_textInputClient];
2132 _hasPlaceholder = YES;
2133 return [[FlutterTextPlaceholder alloc] init];
2134 }
2135
2136 - (void)removeTextPlaceholder:(UITextPlaceholder*)textPlaceholder API_AVAILABLE(ios(13.0)) {
2137 _hasPlaceholder = NO;
2138 [self.textInputDelegate flutterTextInputView:self removeTextPlaceholder:_textInputClient];
2139 }
2140
2141 - (void)deleteBackward {
2142 _selectionAffinity = kTextAffinityDownstream;
2143 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2144 [self resetScribbleInteractionStatusIfEnding];
2145
2146 // When deleting Thai vowel, _selectedTextRange has location
2147 // but does not have length, so we have to manually set it.
2148 // In addition, we needed to delete only a part of grapheme cluster
2149 // because it is the expected behavior of Thai input.
2150 // https://github.com/flutter/flutter/issues/24203
2151 // https://github.com/flutter/flutter/issues/21745
2152 // https://github.com/flutter/flutter/issues/39399
2153 //
2154 // This is needed for correct handling of the deletion of Thai vowel input.
2155 // TODO(cbracken): Get a good understanding of expected behavior of Thai
2156 // input and ensure that this is the correct solution.
2157 // https://github.com/flutter/flutter/issues/28962
2158 if (_selectedTextRange.isEmpty && [self hasText]) {
2159 UITextRange* oldSelectedRange = _selectedTextRange;
2160 NSRange oldRange = ((FlutterTextRange*)oldSelectedRange).range;
2161 if (oldRange.location > 0) {
2162 NSRange newRange = NSMakeRange(oldRange.location - 1, 1);
2163
2164 // We should check if the last character is a part of emoji.
2165 // If so, we must delete the entire emoji to prevent the text from being malformed.
2166 NSRange charRange = fml::RangeForCharacterAtIndex(self.text, oldRange.location - 1);
2167 if (IsEmoji(self.text, charRange)) {
2168 newRange = NSMakeRange(charRange.location, oldRange.location - charRange.location);
2169 }
2170
2171 _selectedTextRange = [[FlutterTextRange rangeWithNSRange:newRange] copy];
2172 }
2173 }
2174
2175 if (!_selectedTextRange.isEmpty) {
2176 // Cache the last deleted emoji to use for an iOS bug where the next
2177 // insertion corrupts the emoji characters.
2178 // See: https://github.com/flutter/flutter/issues/111494#issuecomment-1248441346
2179 if (IsEmoji(self.text, _selectedTextRange.range)) {
2180 NSString* deletedText = [self.text substringWithRange:_selectedTextRange.range];
2181 NSRange deleteFirstCharacterRange = fml::RangeForCharacterAtIndex(deletedText, 0);
2182 self.temporarilyDeletedComposedCharacter =
2183 [deletedText substringWithRange:deleteFirstCharacterRange];
2184 }
2185 [self replaceRange:_selectedTextRange withText:@""];
2186 }
2187 }
2188
2189 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(id)target {
2190 UIAccessibilityPostNotification(notification, target);
2191 }
2192
2193 - (void)accessibilityElementDidBecomeFocused {
2194 if ([self accessibilityElementIsFocused]) {
2195 // For most of the cases, this flutter text input view should never
2196 // receive the focus. If we do receive the focus, we make the best effort
2197 // to send the focus back to the real text field.
2198 FML_DCHECK(_backingTextInputAccessibilityObject);
2199 [self postAccessibilityNotification:UIAccessibilityScreenChangedNotification
2200 target:_backingTextInputAccessibilityObject];
2201 }
2202 }
2203
2204 - (BOOL)accessibilityElementsHidden {
2205 return !_accessibilityEnabled;
2206 }
2207
2208 - (void)resetScribbleInteractionStatusIfEnding {
2209 if (_scribbleInteractionStatus == FlutterScribbleInteractionStatusEnding) {
2210 _scribbleInteractionStatus = FlutterScribbleInteractionStatusNone;
2211 }
2212 }
2213
2214 #pragma mark - Key Events Handling
2215 - (void)pressesBegan:(NSSet<UIPress*>*)presses
2216 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2217 [_textInputPlugin.viewController pressesBegan:presses withEvent:event];
2218 }
2219
2220 - (void)pressesChanged:(NSSet<UIPress*>*)presses
2221 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2222 [_textInputPlugin.viewController pressesChanged:presses withEvent:event];
2223 }
2224
2225 - (void)pressesEnded:(NSSet<UIPress*>*)presses
2226 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2227 [_textInputPlugin.viewController pressesEnded:presses withEvent:event];
2228 }
2229
2230 - (void)pressesCancelled:(NSSet<UIPress*>*)presses
2231 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2232 [_textInputPlugin.viewController pressesCancelled:presses withEvent:event];
2233 }
2234
2235 @end
2236
2237 /**
2238 * Hides `FlutterTextInputView` from iOS accessibility system so it
2239 * does not show up twice, once where it is in the `UIView` hierarchy,
2240 * and a second time as part of the `SemanticsObject` hierarchy.
2241 *
2242 * This prevents the `FlutterTextInputView` from receiving the focus
2243 * due to swiping gesture.
2244 *
2245 * There are other cases the `FlutterTextInputView` may receive
2246 * focus. One example is during screen changes, the accessibility
2247 * tree will undergo a dramatic structural update. The Voiceover may
2248 * decide to focus the `FlutterTextInputView` that is not involved
2249 * in the structural update instead. If that happens, the
2250 * `FlutterTextInputView` will make a best effort to direct the
2251 * focus back to the `SemanticsObject`.
2252 */
2253 @interface FlutterTextInputViewAccessibilityHider : UIView {
2254 }
2255
2256 @end
2257
2258 @implementation FlutterTextInputViewAccessibilityHider {
2259 }
2260
2261 - (BOOL)accessibilityElementsHidden {
2262 return YES;
2263 }
2264
2265 @end
2266
2267 @interface FlutterTextInputPlugin ()
2268 - (void)enableActiveViewAccessibility;
2269 @end
2270
2271 @interface FlutterTimerProxy : NSObject
2272 @property(nonatomic, weak) FlutterTextInputPlugin* target;
2273 @end
2274
2275 @implementation FlutterTimerProxy
2276
2277 + (instancetype)proxyWithTarget:(FlutterTextInputPlugin*)target {
2278 FlutterTimerProxy* proxy = [[self alloc] init];
2279 if (proxy) {
2280 proxy.target = target;
2281 }
2282 return proxy;
2283 }
2284
2285 - (void)enableActiveViewAccessibility {
2286 [self.target enableActiveViewAccessibility];
2287 }
2288
2289 @end
2290
2291 @interface FlutterTextInputPlugin ()
2292 // The current password-autofillable input fields that have yet to be saved.
2293 @property(nonatomic, readonly)
2294 NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
2295 @property(nonatomic, retain) FlutterTextInputView* activeView;
2296 @property(nonatomic, retain) FlutterTextInputViewAccessibilityHider* inputHider;
2297 @property(nonatomic, readonly, weak) id<FlutterViewResponder> viewResponder;
2298
2299 @property(nonatomic, strong) UIView* keyboardViewContainer;
2300 @property(nonatomic, strong) UIView* keyboardView;
2301 @property(nonatomic, strong) UIView* cachedFirstResponder;
2302 @property(nonatomic, assign) CGRect keyboardRect;
2303 @property(nonatomic, assign) CGFloat previousPointerYPosition;
2304 @property(nonatomic, assign) CGFloat pointerYVelocity;
2305 @end
2306
2307 @implementation FlutterTextInputPlugin {
2308 NSTimer* _enableFlutterTextInputViewAccessibilityTimer;
2309 }
2310
2311 - (instancetype)initWithDelegate:(id<FlutterTextInputDelegate>)textInputDelegate {
2312 self = [super init];
2313 if (self) {
2314 // `_textInputDelegate` is a weak reference because it should retain FlutterTextInputPlugin.
2315 _textInputDelegate = textInputDelegate;
2316 _autofillContext = [[NSMutableDictionary alloc] init];
2317 _inputHider = [[FlutterTextInputViewAccessibilityHider alloc] init];
2318 _scribbleElements = [[NSMutableDictionary alloc] init];
2319 _keyboardViewContainer = [[UIView alloc] init];
2320
2321 [[NSNotificationCenter defaultCenter] addObserver:self
2322 selector:@selector(handleKeyboardWillShow:)
2323 name:UIKeyboardWillShowNotification
2324 object:nil];
2325 }
2326
2327 return self;
2328 }
2329
2330 - (void)handleKeyboardWillShow:(NSNotification*)notification {
2331 NSDictionary* keyboardInfo = [notification userInfo];
2332 NSValue* keyboardFrameEnd = [keyboardInfo valueForKey:UIKeyboardFrameEndUserInfoKey];
2333 _keyboardRect = [keyboardFrameEnd CGRectValue];
2334 }
2335
2336 - (void)dealloc {
2337 [self hideTextInput];
2338 }
2339
2340 - (void)removeEnableFlutterTextInputViewAccessibilityTimer {
2341 if (_enableFlutterTextInputViewAccessibilityTimer) {
2342 [_enableFlutterTextInputViewAccessibilityTimer invalidate];
2343 _enableFlutterTextInputViewAccessibilityTimer = nil;
2344 }
2345 }
2346
2347 - (UIView<UITextInput>*)textInputView {
2348 return _activeView;
2349 }
2350
2351 - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
2352 NSString* method = call.method;
2353 id args = call.arguments;
2354 if ([method isEqualToString:kShowMethod]) {
2355 [self showTextInput];
2356 result(nil);
2357 } else if ([method isEqualToString:kHideMethod]) {
2358 [self hideTextInput];
2359 result(nil);
2360 } else if ([method isEqualToString:kSetClientMethod]) {
2361 [self setTextInputClient:[args[0] intValue] withConfiguration:args[1]];
2362 result(nil);
2363 } else if ([method isEqualToString:kSetPlatformViewClientMethod]) {
2364 // This method call has a `platformViewId` argument, but we do not need it for iOS for now.
2365 [self setPlatformViewTextInputClient];
2366 result(nil);
2367 } else if ([method isEqualToString:kSetEditingStateMethod]) {
2368 [self setTextInputEditingState:args];
2369 result(nil);
2370 } else if ([method isEqualToString:kClearClientMethod]) {
2371 [self clearTextInputClient];
2372 result(nil);
2373 } else if ([method isEqualToString:kSetEditableSizeAndTransformMethod]) {
2374 [self setEditableSizeAndTransform:args];
2375 result(nil);
2376 } else if ([method isEqualToString:kSetMarkedTextRectMethod]) {
2377 [self updateMarkedRect:args];
2378 result(nil);
2379 } else if ([method isEqualToString:kFinishAutofillContextMethod]) {
2380 [self triggerAutofillSave:[args boolValue]];
2381 result(nil);
2382 // TODO(justinmc): Remove the TextInput method constant when the framework has
2383 // finished transitioning to using the Scribble channel.
2384 // https://github.com/flutter/flutter/pull/104128
2385 } else if ([method isEqualToString:kDeprecatedSetSelectionRectsMethod]) {
2386 [self setSelectionRects:args];
2387 result(nil);
2388 } else if ([method isEqualToString:kSetSelectionRectsMethod]) {
2389 [self setSelectionRects:args];
2390 result(nil);
2391 } else if ([method isEqualToString:kStartLiveTextInputMethod]) {
2392 [self startLiveTextInput];
2393 result(nil);
2394 } else if ([method isEqualToString:kUpdateConfigMethod]) {
2395 [self updateConfig:args];
2396 result(nil);
2397 } else if ([method isEqualToString:kOnInteractiveKeyboardPointerMoveMethod]) {
2398 CGFloat pointerY = (CGFloat)[args[@"pointerY"] doubleValue];
2399 [self handlePointerMove:pointerY];
2400 result(nil);
2401 } else if ([method isEqualToString:kOnInteractiveKeyboardPointerUpMethod]) {
2402 CGFloat pointerY = (CGFloat)[args[@"pointerY"] doubleValue];
2403 [self handlePointerUp:pointerY];
2404 result(nil);
2405 } else {
2406 result(FlutterMethodNotImplemented);
2407 }
2408 }
2409
2410 - (void)handlePointerUp:(CGFloat)pointerY {
2411 if (_keyboardView.superview != nil) {
2412 // Done to avoid the issue of a pointer up done without a screenshot
2413 // View must be loaded at this point.
2414 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2415 CGFloat screenHeight = screen.bounds.size.height;
2416 CGFloat keyboardHeight = _keyboardRect.size.height;
2417 // Negative velocity indicates a downward movement
2418 BOOL shouldDismissKeyboardBasedOnVelocity = _pointerYVelocity < 0;
2419 [UIView animateWithDuration:kKeyboardAnimationTimeToCompleteion
2420 animations:^{
2421 double keyboardDestination =
2422 shouldDismissKeyboardBasedOnVelocity ? screenHeight : screenHeight - keyboardHeight;
2423 _keyboardViewContainer.frame = CGRectMake(
2424 0, keyboardDestination, _viewController.flutterScreenIfViewLoaded.bounds.size.width,
2425 _keyboardViewContainer.frame.size.height);
2426 }
2427 completion:^(BOOL finished) {
2428 if (shouldDismissKeyboardBasedOnVelocity) {
2429 [self.textInputDelegate flutterTextInputView:self.activeView
2430 didResignFirstResponderWithTextInputClient:self.activeView.textInputClient];
2431 [self dismissKeyboardScreenshot];
2432 } else {
2433 [self showKeyboardAndRemoveScreenshot];
2434 }
2435 }];
2436 }
2437 }
2438
2439 - (void)dismissKeyboardScreenshot {
2440 for (UIView* subView in _keyboardViewContainer.subviews) {
2441 [subView removeFromSuperview];
2442 }
2443 }
2444
2445 - (void)showKeyboardAndRemoveScreenshot {
2446 [UIView setAnimationsEnabled:NO];
2447 [_cachedFirstResponder becomeFirstResponder];
2448 // UIKit does not immediately access the areAnimationsEnabled Boolean so a delay is needed before
2449 // returned
2450 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kKeyboardAnimationDelaySeconds * NSEC_PER_SEC),
2451 dispatch_get_main_queue(), ^{
2452 [UIView setAnimationsEnabled:YES];
2453 [self dismissKeyboardScreenshot];
2454 });
2455 }
2456
2457 - (void)handlePointerMove:(CGFloat)pointerY {
2458 // View must be loaded at this point.
2459 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2460 CGFloat screenHeight = screen.bounds.size.height;
2461 CGFloat keyboardHeight = _keyboardRect.size.height;
2462 if (screenHeight - keyboardHeight <= pointerY) {
2463 // If the pointer is within the bounds of the keyboard.
2464 if (_keyboardView.superview == nil) {
2465 // If no screenshot has been taken.
2466 [self takeKeyboardScreenshotAndDisplay];
2467 [self hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate];
2468 } else {
2469 [self setKeyboardContainerHeight:pointerY];
2470 _pointerYVelocity = _previousPointerYPosition - pointerY;
2471 }
2472 } else {
2473 if (_keyboardView.superview != nil) {
2474 // Keeps keyboard at proper height.
2475 _keyboardViewContainer.frame = _keyboardRect;
2476 _pointerYVelocity = _previousPointerYPosition - pointerY;
2477 }
2478 }
2479 _previousPointerYPosition = pointerY;
2480 }
2481
2482 - (void)setKeyboardContainerHeight:(CGFloat)pointerY {
2483 CGRect frameRect = _keyboardRect;
2484 frameRect.origin.y = pointerY;
2485 _keyboardViewContainer.frame = frameRect;
2486 }
2487
2488 - (void)hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate {
2489 [UIView setAnimationsEnabled:NO];
2490 _cachedFirstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
2491 _activeView.preventCursorDismissWhenResignFirstResponder = YES;
2492 [_cachedFirstResponder resignFirstResponder];
2493 _activeView.preventCursorDismissWhenResignFirstResponder = NO;
2494 [UIView setAnimationsEnabled:YES];
2495 }
2496
2497 - (void)takeKeyboardScreenshotAndDisplay {
2498 // View must be loaded at this point
2499 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2500 UIView* keyboardSnap = [screen snapshotViewAfterScreenUpdates:YES];
2501 keyboardSnap = [keyboardSnap resizableSnapshotViewFromRect:_keyboardRect
2502 afterScreenUpdates:YES
2503 withCapInsets:UIEdgeInsetsZero];
2504 _keyboardView = keyboardSnap;
2505 [_keyboardViewContainer addSubview:_keyboardView];
2506 if (_keyboardViewContainer.superview == nil) {
2507 [UIApplication.sharedApplication.delegate.window.rootViewController.view
2508 addSubview:_keyboardViewContainer];
2509 }
2510 _keyboardViewContainer.layer.zPosition = NSIntegerMax;
2511 _keyboardViewContainer.frame = _keyboardRect;
2512 }
2513
2514 - (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
2515 NSArray* transform = dictionary[@"transform"];
2516 [_activeView setEditableTransform:transform];
2517 const int leftIndex = 12;
2518 const int topIndex = 13;
2519 if ([_activeView isScribbleAvailable]) {
2520 // This is necessary to set up where the scribble interactable element will be.
2521 _inputHider.frame =
2522 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue],
2523 [dictionary[@"width"] intValue], [dictionary[@"height"] intValue]);
2524 _activeView.frame =
2525 CGRectMake(0, 0, [dictionary[@"width"] intValue], [dictionary[@"height"] intValue]);
2526 _activeView.tintColor = [UIColor clearColor];
2527 } else {
2528 // TODO(hellohuanlin): Also need to handle iOS 16 case, where the auto-correction highlight does
2529 // not match the size of text.
2530 // See https://github.com/flutter/flutter/issues/131695
2531 if (@available(iOS 17, *)) {
2532 // Move auto-correction highlight to overlap with the actual text.
2533 // This is to fix an issue where the system auto-correction highlight is displayed at
2534 // the top left corner of the screen on iOS 17+.
2535 // This problem also happens on iOS 16, but the size of highlight does not match the text.
2536 // See https://github.com/flutter/flutter/issues/131695
2537 // TODO(hellohuanlin): Investigate if we can use non-zero size.
2538 _inputHider.frame =
2539 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue], 0, 0);
2540 }
2541 }
2542 }
2543
2544 - (void)updateMarkedRect:(NSDictionary*)dictionary {
2545 NSAssert(dictionary[@"x"] != nil && dictionary[@"y"] != nil && dictionary[@"width"] != nil &&
2546 dictionary[@"height"] != nil,
2547 @"Expected a dictionary representing a CGRect, got %@", dictionary);
2548 CGRect rect = CGRectMake([dictionary[@"x"] doubleValue], [dictionary[@"y"] doubleValue],
2549 [dictionary[@"width"] doubleValue], [dictionary[@"height"] doubleValue]);
2550 _activeView.markedRect = rect.size.width < 0 && rect.size.height < 0 ? kInvalidFirstRect : rect;
2551 }
2552
2553 - (void)setSelectionRects:(NSArray*)encodedRects {
2554 NSMutableArray<FlutterTextSelectionRect*>* rectsAsRect =
2555 [[NSMutableArray alloc] initWithCapacity:[encodedRects count]];
2556 for (NSUInteger i = 0; i < [encodedRects count]; i++) {
2557 NSArray<NSNumber*>* encodedRect = encodedRects[i];
2558 [rectsAsRect addObject:[FlutterTextSelectionRect
2559 selectionRectWithRect:CGRectMake([encodedRect[0] floatValue],
2560 [encodedRect[1] floatValue],
2561 [encodedRect[2] floatValue],
2562 [encodedRect[3] floatValue])
2563 position:[encodedRect[4] unsignedIntegerValue]
2564 writingDirection:[encodedRect[5] unsignedIntegerValue] == 1
2565 ? NSWritingDirectionLeftToRight
2566 : NSWritingDirectionRightToLeft]];
2567 }
2568
2569 // TODO(hellohuanlin): Investigate why notifying the text input system about text changes (via
2570 // textWillChange and textDidChange APIs) causes a bug where we cannot enter text with IME
2571 // keyboards. Issue: https://github.com/flutter/flutter/issues/133908
2572 _activeView.selectionRects = rectsAsRect;
2573 }
2574
2575 - (void)startLiveTextInput {
2576 if (@available(iOS 15.0, *)) {
2577 if (_activeView == nil || !_activeView.isFirstResponder) {
2578 return;
2579 }
2580 [_activeView captureTextFromCamera:nil];
2581 }
2582 }
2583
2584 - (void)showTextInput {
2585 _activeView.viewResponder = _viewResponder;
2586 [self addToInputParentViewIfNeeded:_activeView];
2587 // Adds a delay to prevent the text view from receiving accessibility
2588 // focus in case it is activated during semantics updates.
2589 //
2590 // One common case is when the app navigates to a page with an auto
2591 // focused text field. The text field will activate the FlutterTextInputView
2592 // with a semantics update sent to the engine. The voiceover will focus
2593 // the newly attached active view while performing accessibility update.
2594 // This results in accessibility focus stuck at the FlutterTextInputView.
2595 if (!_enableFlutterTextInputViewAccessibilityTimer) {
2596 _enableFlutterTextInputViewAccessibilityTimer =
2597 [NSTimer scheduledTimerWithTimeInterval:kUITextInputAccessibilityEnablingDelaySeconds
2598 target:[FlutterTimerProxy proxyWithTarget:self]
2599 selector:@selector(enableActiveViewAccessibility)
2600 userInfo:nil
2601 repeats:NO];
2602 }
2603 [_activeView becomeFirstResponder];
2604 }
2605
2606 - (void)enableActiveViewAccessibility {
2607 if (_activeView.isFirstResponder) {
2608 _activeView.accessibilityEnabled = YES;
2609 }
2610 [self removeEnableFlutterTextInputViewAccessibilityTimer];
2611 }
2612
2613 - (void)hideTextInput {
2614 [self removeEnableFlutterTextInputViewAccessibilityTimer];
2615 _activeView.accessibilityEnabled = NO;
2616 [_activeView resignFirstResponder];
2617 [_activeView removeFromSuperview];
2618 [_inputHider removeFromSuperview];
2619 }
2620
2621 - (void)triggerAutofillSave:(BOOL)saveEntries {
2622 [_activeView resignFirstResponder];
2623
2624 if (saveEntries) {
2625 // Make all the input fields in the autofill context visible,
2626 // then remove them to trigger autofill save.
2627 [self cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
2628 [_autofillContext removeAllObjects];
2629 [self changeInputViewsAutofillVisibility:YES];
2630 } else {
2631 [_autofillContext removeAllObjects];
2632 }
2633
2634 [self cleanUpViewHierarchy:YES clearText:!saveEntries delayRemoval:NO];
2635 [self addToInputParentViewIfNeeded:_activeView];
2636 }
2637
2638 - (void)setPlatformViewTextInputClient {
2639 // No need to track the platformViewID (unlike in Android). When a platform view
2640 // becomes the first responder, simply hide this dummy text input view (`_activeView`)
2641 // for the previously focused widget.
2642 [self removeEnableFlutterTextInputViewAccessibilityTimer];
2643 _activeView.accessibilityEnabled = NO;
2644 [_activeView removeFromSuperview];
2645 [_inputHider removeFromSuperview];
2646 }
2647
2648 - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configuration {
2649 [self resetAllClientIds];
2650 // Hide all input views from autofill, only make those in the new configuration visible
2651 // to autofill.
2652 [self changeInputViewsAutofillVisibility:NO];
2653
2654 // Update the current active view.
2655 switch (AutofillTypeOf(configuration)) {
2656 case kFlutterAutofillTypeNone:
2657 self.activeView = [self createInputViewWith:configuration];
2658 break;
2659 case kFlutterAutofillTypeRegular:
2660 // If the group does not involve password autofill, only install the
2661 // input view that's being focused.
2662 self.activeView = [self updateAndShowAutofillViews:nil
2663 focusedField:configuration
2664 isPasswordRelated:NO];
2665 break;
2666 case kFlutterAutofillTypePassword:
2667 self.activeView = [self updateAndShowAutofillViews:configuration[kAssociatedAutofillFields]
2668 focusedField:configuration
2669 isPasswordRelated:YES];
2670 break;
2671 }
2672 [_activeView setTextInputClient:client];
2673 [_activeView reloadInputViews];
2674
2675 // Clean up views that no longer need to be in the view hierarchy, according to
2676 // the current autofill context. The "garbage" input views are already made
2677 // invisible to autofill and they can't `becomeFirstResponder`, we only remove
2678 // them to free up resources and reduce the number of input views in the view
2679 // hierarchy.
2680 //
2681 // The garbage views are decommissioned immediately, but the removeFromSuperview
2682 // call is scheduled on the runloop and delayed by 0.1s so we don't remove the
2683 // text fields immediately (which seems to make the keyboard flicker).
2684 // See: https://github.com/flutter/flutter/issues/64628.
2685 [self cleanUpViewHierarchy:NO clearText:YES delayRemoval:YES];
2686 }
2687
2688 // Creates and shows an input field that is not password related and has no autofill
2689 // info. This method returns a new FlutterTextInputView instance when called, since
2690 // UIKit uses the identity of `UITextInput` instances (or the identity of the input
2691 // views) to decide whether the IME's internal states should be reset. See:
2692 // https://github.com/flutter/flutter/issues/79031 .
2693 - (FlutterTextInputView*)createInputViewWith:(NSDictionary*)configuration {
2694 NSString* autofillId = AutofillIdFromDictionary(configuration);
2695 if (autofillId) {
2696 [_autofillContext removeObjectForKey:autofillId];
2697 }
2698 FlutterTextInputView* newView = [[FlutterTextInputView alloc] initWithOwner:self];
2699 [newView configureWithDictionary:configuration];
2700 [self addToInputParentViewIfNeeded:newView];
2701
2702 for (NSDictionary* field in configuration[kAssociatedAutofillFields]) {
2703 NSString* autofillId = AutofillIdFromDictionary(field);
2704 if (autofillId && AutofillTypeOf(field) == kFlutterAutofillTypeNone) {
2705 [_autofillContext removeObjectForKey:autofillId];
2706 }
2707 }
2708 return newView;
2709 }
2710
2711 - (FlutterTextInputView*)updateAndShowAutofillViews:(NSArray*)fields
2712 focusedField:(NSDictionary*)focusedField
2713 isPasswordRelated:(BOOL)isPassword {
2714 FlutterTextInputView* focused = nil;
2715 NSString* focusedId = AutofillIdFromDictionary(focusedField);
2716 NSAssert(focusedId, @"autofillId must not be null for the focused field: %@", focusedField);
2717
2718 if (!fields) {
2719 // DO NOT push the current autofillable input fields to the context even
2720 // if it's password-related, because it is not in an autofill group.
2721 focused = [self getOrCreateAutofillableView:focusedField isPasswordAutofill:isPassword];
2722 [_autofillContext removeObjectForKey:focusedId];
2723 }
2724
2725 for (NSDictionary* field in fields) {
2726 NSString* autofillId = AutofillIdFromDictionary(field);
2727 NSAssert(autofillId, @"autofillId must not be null for field: %@", field);
2728
2729 BOOL hasHints = AutofillTypeOf(field) != kFlutterAutofillTypeNone;
2730 BOOL isFocused = [focusedId isEqualToString:autofillId];
2731
2732 if (isFocused) {
2733 focused = [self getOrCreateAutofillableView:field isPasswordAutofill:isPassword];
2734 }
2735
2736 if (hasHints) {
2737 // Push the current input field to the context if it has hints.
2738 _autofillContext[autofillId] = isFocused ? focused
2739 : [self getOrCreateAutofillableView:field
2740 isPasswordAutofill:isPassword];
2741 } else {
2742 // Mark for deletion.
2743 [_autofillContext removeObjectForKey:autofillId];
2744 }
2745 }
2746
2747 NSAssert(focused, @"The current focused input view must not be nil.");
2748 return focused;
2749 }
2750
2751 // Returns a new non-reusable input view (and put it into the view hierarchy), or get the
2752 // view from the current autofill context, if an input view with the same autofill id
2753 // already exists in the context.
2754 // This is generally used for input fields that are autofillable (UIKit tracks these veiws
2755 // for autofill purposes so they should not be reused for a different type of views).
2756 - (FlutterTextInputView*)getOrCreateAutofillableView:(NSDictionary*)field
2757 isPasswordAutofill:(BOOL)needsPasswordAutofill {
2758 NSString* autofillId = AutofillIdFromDictionary(field);
2759 FlutterTextInputView* inputView = _autofillContext[autofillId];
2760 if (!inputView) {
2761 inputView =
2762 needsPasswordAutofill ? [FlutterSecureTextInputView alloc] : [FlutterTextInputView alloc];
2763 inputView = [inputView initWithOwner:self];
2764 [self addToInputParentViewIfNeeded:inputView];
2765 }
2766
2767 [inputView configureWithDictionary:field];
2768 return inputView;
2769 }
2770
2771 // The UIView to add FlutterTextInputViews to.
2772 - (UIView*)hostView {
2773 UIView* host = _viewController.view;
2774 NSAssert(host != nullptr,
2775 @"The application must have a host view since the keyboard client "
2776 @"must be part of the responder chain to function. The host view controller is %@",
2777 _viewController);
2778 return host;
2779 }
2780
2781 // The UIView to add FlutterTextInputViews to.
2782 - (NSArray<UIView*>*)textInputViews {
2783 return _inputHider.subviews;
2784 }
2785
2786 // Removes every installed input field, unless it's in the current autofill context.
2787 //
2788 // The active view will be removed from its superview too, if includeActiveView is YES.
2789 // When clearText is YES, the text on the input fields will be set to empty before
2790 // they are removed from the view hierarchy, to avoid triggering autofill save.
2791 // If delayRemoval is true, removeFromSuperview will be scheduled on the runloop and
2792 // will be delayed by 0.1s so we don't remove the text fields immediately (which seems
2793 // to make the keyboard flicker).
2794 // See: https://github.com/flutter/flutter/issues/64628.
2795
2796 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
2797 clearText:(BOOL)clearText
2798 delayRemoval:(BOOL)delayRemoval {
2799 for (UIView* view in self.textInputViews) {
2800 if ([view isKindOfClass:[FlutterTextInputView class]] &&
2801 (includeActiveView || view != _activeView)) {
2802 FlutterTextInputView* inputView = (FlutterTextInputView*)view;
2803 if (_autofillContext[inputView.autofillId] != view) {
2804 if (clearText) {
2805 [inputView replaceRangeLocal:NSMakeRange(0, inputView.text.length) withText:@""];
2806 }
2807 if (delayRemoval) {
2808 [inputView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:0.1];
2809 } else {
2810 [inputView removeFromSuperview];
2811 }
2812 }
2813 }
2814 }
2815 }
2816
2817 // Changes the visibility of every FlutterTextInputView currently in the
2818 // view hierarchy.
2819 - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
2820 for (UIView* view in self.textInputViews) {
2821 if ([view isKindOfClass:[FlutterTextInputView class]]) {
2822 FlutterTextInputView* inputView = (FlutterTextInputView*)view;
2823 inputView.isVisibleToAutofill = newVisibility;
2824 }
2825 }
2826 }
2827
2828 // Resets the client id of every FlutterTextInputView in the view hierarchy
2829 // to 0.
2830 // Called before establishing a new text input connection.
2831 // For views in the current autofill context, they need to
2832 // stay in the view hierachy but should not be allowed to
2833 // send messages (other than autofill related ones) to the
2834 // framework.
2835 - (void)resetAllClientIds {
2836 for (UIView* view in self.textInputViews) {
2837 if ([view isKindOfClass:[FlutterTextInputView class]]) {
2838 FlutterTextInputView* inputView = (FlutterTextInputView*)view;
2839 [inputView setTextInputClient:0];
2840 }
2841 }
2842 }
2843
2844 - (void)addToInputParentViewIfNeeded:(FlutterTextInputView*)inputView {
2845 if (![inputView isDescendantOfView:_inputHider]) {
2846 [_inputHider addSubview:inputView];
2847 }
2848
2849 if (_viewController.view == nil) {
2850 // If view controller's view has detached from flutter engine, we don't add _inputHider
2851 // in parent view to fallback and avoid crash.
2852 // https://github.com/flutter/flutter/issues/106404.
2853 return;
2854 }
2855 UIView* parentView = self.hostView;
2856 if (_inputHider.superview != parentView) {
2857 [parentView addSubview:_inputHider];
2858 }
2859 }
2860
2861 - (void)setTextInputEditingState:(NSDictionary*)state {
2862 [_activeView setTextInputState:state];
2863 }
2864
2865 - (void)clearTextInputClient {
2866 [_activeView setTextInputClient:0];
2867 _activeView.frame = CGRectZero;
2868 }
2869
2870 - (void)updateConfig:(NSDictionary*)dictionary {
2871 BOOL isSecureTextEntry = [dictionary[kSecureTextEntry] boolValue];
2872 for (UIView* view in self.textInputViews) {
2873 if ([view isKindOfClass:[FlutterTextInputView class]]) {
2874 FlutterTextInputView* inputView = (FlutterTextInputView*)view;
2875 // The feature of holding and draging spacebar to move cursor is affected by
2876 // secureTextEntry, so when obscureText is updated, we need to update secureTextEntry
2877 // and call reloadInputViews.
2878 // https://github.com/flutter/flutter/issues/122139
2879 if (inputView.isSecureTextEntry != isSecureTextEntry) {
2880 inputView.secureTextEntry = isSecureTextEntry;
2881 [inputView reloadInputViews];
2882 }
2883 }
2884 }
2885 }
2886
2887 #pragma mark UIIndirectScribbleInteractionDelegate
2888
2889 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2890 isElementFocused:(UIScribbleElementIdentifier)elementIdentifier
2891 API_AVAILABLE(ios(14.0)) {
2892 return _activeView.scribbleFocusStatus == FlutterScribbleFocusStatusFocused;
2893 }
2894
2895 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2896 focusElementIfNeeded:(UIScribbleElementIdentifier)elementIdentifier
2897 referencePoint:(CGPoint)focusReferencePoint
2898 completion:(void (^)(UIResponder<UITextInput>* focusedInput))completion
2899 API_AVAILABLE(ios(14.0)) {
2900 _activeView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
2901 [_indirectScribbleDelegate flutterTextInputPlugin:self
2902 focusElement:elementIdentifier
2903 atPoint:focusReferencePoint
2904 result:^(id _Nullable result) {
2905 _activeView.scribbleFocusStatus =
2906 FlutterScribbleFocusStatusFocused;
2907 completion(_activeView);
2908 }];
2909 }
2910
2911 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2912 shouldDelayFocusForElement:(UIScribbleElementIdentifier)elementIdentifier
2913 API_AVAILABLE(ios(14.0)) {
2914 return NO;
2915 }
2916
2917 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2918 willBeginWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
2919 API_AVAILABLE(ios(14.0)) {
2920 }
2921
2922 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2923 didFinishWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
2924 API_AVAILABLE(ios(14.0)) {
2925 }
2926
2927 - (CGRect)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2928 frameForElement:(UIScribbleElementIdentifier)elementIdentifier
2929 API_AVAILABLE(ios(14.0)) {
2930 NSValue* elementValue = [_scribbleElements objectForKey:elementIdentifier];
2931 if (elementValue == nil) {
2932 return CGRectZero;
2933 }
2934 return [elementValue CGRectValue];
2935 }
2936
2937 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2938 requestElementsInRect:(CGRect)rect
2939 completion:
2940 (void (^)(NSArray<UIScribbleElementIdentifier>* elements))completion
2941 API_AVAILABLE(ios(14.0)) {
2942 [_indirectScribbleDelegate
2943 flutterTextInputPlugin:self
2944 requestElementsInRect:rect
2945 result:^(id _Nullable result) {
2946 NSMutableArray<UIScribbleElementIdentifier>* elements =
2947 [[NSMutableArray alloc] init];
2948 if ([result isKindOfClass:[NSArray class]]) {
2949 for (NSArray* elementArray in result) {
2950 [elements addObject:elementArray[0]];
2951 [_scribbleElements
2952 setObject:[NSValue
2953 valueWithCGRect:CGRectMake(
2954 [elementArray[1] floatValue],
2955 [elementArray[2] floatValue],
2956 [elementArray[3] floatValue],
2957 [elementArray[4] floatValue])]
2958 forKey:elementArray[0]];
2959 }
2960 }
2961 completion(elements);
2962 }];
2963 }
2964
2965 #pragma mark - Methods related to Scribble support
2966
2967 - (void)setUpIndirectScribbleInteraction:(id<FlutterViewResponder>)viewResponder {
2968 if (_viewResponder != viewResponder) {
2969 if (@available(iOS 14.0, *)) {
2970 UIView* parentView = viewResponder.view;
2971 if (parentView != nil) {
2972 UIIndirectScribbleInteraction* scribbleInteraction = [[UIIndirectScribbleInteraction alloc]
2973 initWithDelegate:(id<UIIndirectScribbleInteractionDelegate>)self];
2974 [parentView addInteraction:scribbleInteraction];
2975 }
2976 }
2977 }
2978 _viewResponder = viewResponder;
2979 }
2980
2981 - (void)resetViewResponder {
2982 _viewResponder = nil;
2983 }
2984
2985 #pragma mark -
2986 #pragma mark FlutterKeySecondaryResponder
2987
2988 /**
2989 * Handles key down events received from the view controller, responding YES if
2990 * the event was handled.
2991 */
2992 - (BOOL)handlePress:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4)) {
2993 return NO;
2994 }
2995 @end
2996
2997 /**
2998 * Recursively searches the UIView's subviews to locate the First Responder
2999 */
3000 @implementation UIView (FindFirstResponder)
3001 - (id)flutterFirstResponder {
3002 if (self.isFirstResponder) {
3003 return self;
3004 }
3005 for (UIView* subView in self.subviews) {
3006 UIView* firstResponder = subView.flutterFirstResponder;
3007 if (firstResponder) {
3008 return firstResponder;
3009 }
3010 }
3011 return nil;
3012 }
3013 @end
FlutterTextSelectionRect::writingDirection
NSWritingDirection writingDirection
Definition: FlutterTextInputPlugin.h:95
IsEmoji
static BOOL IsEmoji(NSString *text, NSRange charRange)
Definition: FlutterTextInputPlugin.mm:85
ToUITextContentType
static UITextContentType ToUITextContentType(NSArray< NSString * > *hints)
Definition: FlutterTextInputPlugin.mm:211
caretRectForPosition
CGRect caretRectForPosition
Definition: FlutterTextInputPlugin.h:173
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:392
FlutterTextSelectionRect::containsStart
BOOL containsStart
Definition: FlutterTextInputPlugin.h:96
returnKeyType
UIReturnKeyType returnKeyType
Definition: FlutterTextInputPlugin.h:148
FlutterSecureTextInputView::textField
UITextField * textField
Definition: FlutterTextInputPlugin.mm:746
_scribbleInteractionStatus
FlutterScribbleInteractionStatus _scribbleInteractionStatus
Definition: FlutterTextInputPlugin.mm:807
FlutterTextInputDelegate-p
Definition: FlutterTextInputDelegate.h:37
_viewController
fml::WeakNSObject< FlutterViewController > _viewController
Definition: FlutterEngine.mm:122
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:146
kAutocorrectionType
static NSString *const kAutocorrectionType
Definition: FlutterTextInputPlugin.mm:80
isScribbleAvailable
BOOL isScribbleAvailable
Definition: FlutterTextInputPlugin.h:162
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:132
_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:88
FlutterTextSelectionRect::containsEnd
BOOL containsEnd
Definition: FlutterTextInputPlugin.h:97
kSmartQuotesType
static NSString *const kSmartQuotesType
Definition: FlutterTextInputPlugin.mm:70
FlutterTextSelectionRect::rect
CGRect rect
Definition: FlutterTextInputPlugin.h:93
resetScribbleInteractionStatusIfEnding
void resetScribbleInteractionStatusIfEnding
Definition: FlutterTextInputPlugin.h:161
FlutterMethodCall::method
NSString * method
Definition: FlutterCodecs.h:233
kSetPlatformViewClientMethod
static NSString *const kSetPlatformViewClientMethod
Definition: FlutterTextInputPlugin.mm:42
FlutterTimerProxy
Definition: FlutterTextInputPlugin.mm:2271
kSetEditableSizeAndTransformMethod
static NSString *const kSetEditableSizeAndTransformMethod
Definition: FlutterTextInputPlugin.mm:45
kAutofillId
static NSString *const kAutofillId
Definition: FlutterTextInputPlugin.mm:76
FlutterTextRange
Definition: FlutterTextInputPlugin.h:79
ToUIReturnKeyType
static UIReturnKeyType ToUIReturnKeyType(NSString *inputType)
Definition: FlutterTextInputPlugin.mm:158
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:98
initWithOwner
instancetype initWithOwner
Definition: FlutterTextInputPlugin.h:168
_isSystemKeyboardEnabled
bool _isSystemKeyboardEnabled
Definition: FlutterTextInputPlugin.mm:812
kInvalidFirstRect
const CGRect kInvalidFirstRect
Definition: FlutterTextInputPlugin.mm:35
_isFloatingCursorActive
bool _isFloatingCursorActive
Definition: FlutterTextInputPlugin.mm:813
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:323
FlutterTextInputView
Definition: FlutterTextInputPlugin.mm:801
_textField
UITextField * _textField
Definition: FlutterPlatformPlugin.mm:76
UIView(FindFirstResponder)
Definition: FlutterTextInputPlugin.h:176
selectedTextRange
API_AVAILABLE(ios(13.0)) @interface FlutterTextPlaceholder UITextRange * selectedTextRange
Definition: FlutterTextInputPlugin.h:125
FlutterMethodCall
Definition: FlutterCodecs.h:220
NS_ENUM
typedef NS_ENUM(NSInteger, FlutterAutofillType)
Definition: FlutterTextInputPlugin.mm:383
_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:81
FlutterTextInputPlugin
Definition: FlutterTextInputPlugin.h:33
localRectFromFrameworkTransform
CGRect localRectFromFrameworkTransform
Definition: FlutterTextInputPlugin.h:172
FlutterTextPosition::affinity
UITextStorageDirection affinity
Definition: FlutterTextInputPlugin.h:70
FlutterResult
void(^ FlutterResult)(id _Nullable result)
Definition: FlutterChannels.h:194
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:94
kOnInteractiveKeyboardPointerMoveMethod
static NSString *const kOnInteractiveKeyboardPointerMoveMethod
Definition: FlutterTextInputPlugin.mm:56
FlutterTextInputViewAccessibilityHider
Definition: FlutterTextInputPlugin.mm:2253
inputDelegate
id< UITextInputDelegate > inputDelegate
Definition: FlutterTextInputPlugin.h:138
_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:158
kEnableDeltaModel
static NSString *const kEnableDeltaModel
Definition: FlutterTextInputPlugin.mm:66
kKeyboardAnimationDelaySeconds
static const NSTimeInterval kKeyboardAnimationDelaySeconds
Definition: FlutterTextInputPlugin.mm:26
FlutterTextPosition
Definition: FlutterTextInputPlugin.h:67
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:2272
FlutterTextPosition::index
NSUInteger index
Definition: FlutterTextInputPlugin.h:69
AutofillTypeOf
static FlutterAutofillType AutofillTypeOf(NSDictionary *configuration)
Definition: FlutterTextInputPlugin.mm:418
FlutterTextSelectionRect
Definition: FlutterTextInputPlugin.h:91
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
FLUTTER_ASSERT_ARC
Definition: VsyncWaiterIosTest.mm:15
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:146
markedTextRange
UITextRange * markedTextRange
Definition: FlutterTextInputPlugin.h:136
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