| Flutter iOS Embedder |

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