| Flutter iOS Embedder |

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