| Flutter iOS Embedder |

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