| Flutter iOS Embedder |

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