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