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