| 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 }
1007 }
1008 return [UIMenu menuWithChildren:items];
1009 }
1010
1011 - (void)editMenuInteraction:(UIEditMenuInteraction*)interaction
1012 willDismissMenuForConfiguration:(UIEditMenuConfiguration*)configuration
1013 animator:(id<UIEditMenuInteractionAnimating>)animator
1014 API_AVAILABLE(ios(16.0)) {
1015 [self.textInputDelegate flutterTextInputView:self
1016 willDismissEditMenuWithTextInputClient:_textInputClient];
1017 }
1018
1019 - (CGRect)editMenuInteraction:(UIEditMenuInteraction*)interaction
1020 targetRectForConfiguration:(UIEditMenuConfiguration*)configuration API_AVAILABLE(ios(16.0)) {
1021 return _editMenuTargetRect;
1022 }
1023
1024 - (void)showEditMenuWithTargetRect:(CGRect)targetRect
1025 items:(NSArray<NSDictionary*>*)items API_AVAILABLE(ios(16.0)) {
1026 _editMenuTargetRect = targetRect;
1027 _editMenuItems = items;
1028 UIEditMenuConfiguration* config =
1029 [UIEditMenuConfiguration configurationWithIdentifier:nil sourcePoint:CGPointZero];
1030 [self.editMenuInteraction presentEditMenuWithConfiguration:config];
1031 }
1032
1033 - (void)hideEditMenu API_AVAILABLE(ios(16.0)) {
1034 [self.editMenuInteraction dismissMenu];
1035 }
1036
1037 - (void)configureWithDictionary:(NSDictionary*)configuration {
1038 NSDictionary* inputType = configuration[kKeyboardType];
1039 NSString* keyboardAppearance = configuration[kKeyboardAppearance];
1040 NSDictionary* autofill = configuration[kAutofillProperties];
1041
1042 self.secureTextEntry = [configuration[kSecureTextEntry] boolValue];
1043 self.enableDeltaModel = [configuration[kEnableDeltaModel] boolValue];
1044
1045 _isSystemKeyboardEnabled = ShouldShowSystemKeyboard(inputType);
1046 self.keyboardType = ToUIKeyboardType(inputType);
1047 self.returnKeyType = ToUIReturnKeyType(configuration[kInputAction]);
1048 self.autocapitalizationType = ToUITextAutoCapitalizationType(configuration);
1049 _enableInteractiveSelection = [configuration[kEnableInteractiveSelection] boolValue];
1050 NSString* smartDashesType = configuration[kSmartDashesType];
1051 // This index comes from the SmartDashesType enum in the framework.
1052 bool smartDashesIsDisabled = smartDashesType && [smartDashesType isEqualToString:@"0"];
1053 self.smartDashesType = smartDashesIsDisabled ? UITextSmartDashesTypeNo : UITextSmartDashesTypeYes;
1054 NSString* smartQuotesType = configuration[kSmartQuotesType];
1055 // This index comes from the SmartQuotesType enum in the framework.
1056 bool smartQuotesIsDisabled = smartQuotesType && [smartQuotesType isEqualToString:@"0"];
1057 self.smartQuotesType = smartQuotesIsDisabled ? UITextSmartQuotesTypeNo : UITextSmartQuotesTypeYes;
1058 if ([keyboardAppearance isEqualToString:@"Brightness.dark"]) {
1059 self.keyboardAppearance = UIKeyboardAppearanceDark;
1060 } else if ([keyboardAppearance isEqualToString:@"Brightness.light"]) {
1061 self.keyboardAppearance = UIKeyboardAppearanceLight;
1062 } else {
1063 self.keyboardAppearance = UIKeyboardAppearanceDefault;
1064 }
1065 NSString* autocorrect = configuration[kAutocorrectionType];
1066 bool autocorrectIsDisabled = autocorrect && ![autocorrect boolValue];
1067 self.autocorrectionType =
1068 autocorrectIsDisabled ? UITextAutocorrectionTypeNo : UITextAutocorrectionTypeDefault;
1069 self.spellCheckingType =
1070 autocorrectIsDisabled ? UITextSpellCheckingTypeNo : UITextSpellCheckingTypeDefault;
1071 self.autofillId = AutofillIdFromDictionary(configuration);
1072 if (autofill == nil) {
1073 self.textContentType = @"";
1074 } else {
1075 self.textContentType = ToUITextContentType(autofill[kAutofillHints]);
1076 [self setTextInputState:autofill[kAutofillEditingValue]];
1077 NSAssert(_autofillId, @"The autofill configuration must contain an autofill id");
1078 }
1079 // The input field needs to be visible for the system autofill
1080 // to find it.
1081 self.isVisibleToAutofill = autofill || _secureTextEntry;
1082 }
1083
1084 - (UITextContentType)textContentType {
1085 return _textContentType;
1086 }
1087
1088 // Prevent UIKit from showing selection handles or highlights. This is needed
1089 // because Scribble interactions require the view to have it's actual frame on
1090 // the screen. They're not needed on iOS 17 with the new
1091 // UITextSelectionDisplayInteraction API.
1092 //
1093 // These are undocumented methods. On iOS 17, the insertion point color is also
1094 // used as the highlighted background of the selected IME candidate:
1095 // https://github.com/flutter/flutter/issues/132548
1096 // So the respondsToSelector method is overridden to return NO for this method
1097 // on iOS 17+.
1098 - (UIColor*)insertionPointColor {
1099 return [UIColor clearColor];
1100 }
1101
1102 - (UIColor*)selectionBarColor {
1103 return [UIColor clearColor];
1104 }
1105
1106 - (UIColor*)selectionHighlightColor {
1107 return [UIColor clearColor];
1108 }
1109
1110 - (UIInputViewController*)inputViewController {
1111 if (_isSystemKeyboardEnabled) {
1112 return nil;
1113 }
1114
1115 if (!_inputViewController) {
1116 _inputViewController = [[UIInputViewController alloc] init];
1117 }
1118 return _inputViewController;
1119 }
1120
1121 - (id<FlutterTextInputDelegate>)textInputDelegate {
1122 return _textInputPlugin.textInputDelegate;
1123 }
1124
1125 - (BOOL)respondsToSelector:(SEL)selector {
1126 if (@available(iOS 17.0, *)) {
1127 // See the comment on this method.
1128 if (selector == @selector(insertionPointColor)) {
1129 return NO;
1130 }
1131 }
1132 return [super respondsToSelector:selector];
1133 }
1134
1135 - (void)setTextInputClient:(int)client {
1136 _textInputClient = client;
1137 _hasPlaceholder = NO;
1138 }
1139
1140 - (UITextInteraction*)textInteraction API_AVAILABLE(ios(13.0)) {
1141 if (!_textInteraction) {
1142 _textInteraction = [UITextInteraction textInteractionForMode:UITextInteractionModeEditable];
1143 _textInteraction.textInput = self;
1144 }
1145 return _textInteraction;
1146 }
1147
1148 - (void)setTextInputState:(NSDictionary*)state {
1149 // [UITextInteraction willMoveToView:] sometimes sets the textInput's inputDelegate
1150 // to nil. This is likely a bug in UIKit. In order to inform the keyboard of text
1151 // and selection changes when that happens, add a dummy UITextInteraction to this
1152 // view so it sets a valid inputDelegate that we can call textWillChange et al. on.
1153 // See https://github.com/flutter/engine/pull/32881.
1154 if (!self.inputDelegate && self.isFirstResponder) {
1155 [self addInteraction:self.textInteraction];
1156 }
1157
1158 NSString* newText = state[@"text"];
1159 BOOL textChanged = ![self.text isEqualToString:newText];
1160 if (textChanged) {
1161 [self.inputDelegate textWillChange:self];
1162 [self.text setString:newText];
1163 }
1164 NSInteger composingBase = [state[@"composingBase"] intValue];
1165 NSInteger composingExtent = [state[@"composingExtent"] intValue];
1166 NSRange composingRange = [self clampSelection:NSMakeRange(MIN(composingBase, composingExtent),
1167 ABS(composingBase - composingExtent))
1168 forText:self.text];
1169
1170 self.markedTextRange =
1171 composingRange.length > 0 ? [FlutterTextRange rangeWithNSRange:composingRange] : nil;
1172
1173 NSRange selectedRange = [self clampSelectionFromBase:[state[@"selectionBase"] intValue]
1174 extent:[state[@"selectionExtent"] intValue]
1175 forText:self.text];
1176
1177 NSRange oldSelectedRange = [(FlutterTextRange*)self.selectedTextRange range];
1178 if (!NSEqualRanges(selectedRange, oldSelectedRange)) {
1179 [self.inputDelegate selectionWillChange:self];
1180
1181 [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]];
1182
1183 _selectionAffinity = kTextAffinityDownstream;
1184 if ([state[@"selectionAffinity"] isEqualToString:@(kTextAffinityUpstream)]) {
1185 _selectionAffinity = kTextAffinityUpstream;
1186 }
1187 [self.inputDelegate selectionDidChange:self];
1188 }
1189
1190 if (textChanged) {
1191 [self.inputDelegate textDidChange:self];
1192 }
1193
1194 if (_textInteraction) {
1195 [self removeInteraction:_textInteraction];
1196 }
1197 }
1198
1199 // Forward touches to the viewResponder to allow tapping inside the UITextField as normal.
1200 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
1201 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1202 [self resetScribbleInteractionStatusIfEnding];
1203 [self.viewResponder touchesBegan:touches withEvent:event];
1204 }
1205
1206 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
1207 [self.viewResponder touchesMoved:touches withEvent:event];
1208 }
1209
1210 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
1211 [self.viewResponder touchesEnded:touches withEvent:event];
1212 }
1213
1214 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1215 [self.viewResponder touchesCancelled:touches withEvent:event];
1216 }
1217
1218 - (void)touchesEstimatedPropertiesUpdated:(NSSet*)touches {
1219 [self.viewResponder touchesEstimatedPropertiesUpdated:touches];
1220 }
1221
1222 // Extracts the selection information from the editing state dictionary.
1223 //
1224 // The state may contain an invalid selection, such as when no selection was
1225 // explicitly set in the framework. This is handled here by setting the
1226 // selection to (0,0). In contrast, Android handles this situation by
1227 // clearing the selection, but the result in both cases is that the cursor
1228 // is placed at the beginning of the field.
1229 - (NSRange)clampSelectionFromBase:(int)selectionBase
1230 extent:(int)selectionExtent
1231 forText:(NSString*)text {
1232 int loc = MIN(selectionBase, selectionExtent);
1233 int len = ABS(selectionExtent - selectionBase);
1234 return loc < 0 ? NSMakeRange(0, 0)
1235 : [self clampSelection:NSMakeRange(loc, len) forText:self.text];
1236 }
1237
1238 - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text {
1239 NSUInteger start = MIN(MAX(range.location, 0), text.length);
1240 NSUInteger length = MIN(range.length, text.length - start);
1241 return NSMakeRange(start, length);
1242 }
1243
1244 - (BOOL)isVisibleToAutofill {
1245 return self.frame.size.width > 0 && self.frame.size.height > 0;
1246 }
1247
1248 // An input view is generally ignored by password autofill attempts, if it's
1249 // not the first responder and is zero-sized. For input fields that are in the
1250 // autofill context but do not belong to the current autofill group, setting
1251 // their frames to CGRectZero prevents ios autofill from taking them into
1252 // account.
1253 - (void)setIsVisibleToAutofill:(BOOL)isVisibleToAutofill {
1254 // This probably needs to change (think it is getting overwritten by the updateSizeAndTransform
1255 // stuff for now).
1256 self.frame = isVisibleToAutofill ? CGRectMake(0, 0, 1, 1) : CGRectZero;
1257 }
1258
1259 #pragma mark UIScribbleInteractionDelegate
1260
1261 // Checks whether Scribble features are possibly available – meaning this is an iPad running iOS
1262 // 14 or higher.
1263 - (BOOL)isScribbleAvailable {
1264 if (@available(iOS 14.0, *)) {
1265 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
1266 return YES;
1267 }
1268 }
1269 return NO;
1270 }
1271
1272 - (void)scribbleInteractionWillBeginWriting:(UIScribbleInteraction*)interaction
1273 API_AVAILABLE(ios(14.0)) {
1274 _scribbleInteractionStatus = FlutterScribbleInteractionStatusStarted;
1275 [self.textInputDelegate flutterTextInputViewScribbleInteractionBegan:self];
1276 }
1277
1278 - (void)scribbleInteractionDidFinishWriting:(UIScribbleInteraction*)interaction
1279 API_AVAILABLE(ios(14.0)) {
1280 _scribbleInteractionStatus = FlutterScribbleInteractionStatusEnding;
1281 [self.textInputDelegate flutterTextInputViewScribbleInteractionFinished:self];
1282 }
1283
1284 - (BOOL)scribbleInteraction:(UIScribbleInteraction*)interaction
1285 shouldBeginAtLocation:(CGPoint)location API_AVAILABLE(ios(14.0)) {
1286 return YES;
1287 }
1288
1289 - (BOOL)scribbleInteractionShouldDelayFocus:(UIScribbleInteraction*)interaction
1290 API_AVAILABLE(ios(14.0)) {
1291 return NO;
1292 }
1293
1294 #pragma mark - UIResponder Overrides
1295
1296 - (BOOL)canBecomeFirstResponder {
1297 // Only the currently focused input field can
1298 // become the first responder. This prevents iOS
1299 // from changing focus by itself (the framework
1300 // focus will be out of sync if that happens).
1301 return _textInputClient != 0;
1302 }
1303
1304 - (BOOL)resignFirstResponder {
1305 BOOL success = [super resignFirstResponder];
1306 if (success) {
1307 if (!_preventCursorDismissWhenResignFirstResponder) {
1308 [self.textInputDelegate flutterTextInputView:self
1309 didResignFirstResponderWithTextInputClient:_textInputClient];
1310 }
1311 }
1312 return success;
1313 }
1314
1315 - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
1316 if (action == @selector(paste:)) {
1317 // Forbid pasting images, memojis, or other non-string content.
1318 return [UIPasteboard generalPasteboard].hasStrings;
1319 } else if (action == @selector(copy:) || action == @selector(cut:) ||
1320 action == @selector(delete:)) {
1321 return [self textInRange:_selectedTextRange].length > 0;
1322 } else if (action == @selector(selectAll:)) {
1323 return self.hasText;
1324 } else if (action == @selector(captureTextFromCamera:)) {
1325 if (@available(iOS 15.0, *)) {
1326 return YES;
1327 }
1328 return NO;
1329 }
1330 return [super canPerformAction:action withSender:sender];
1331 }
1332
1333 #pragma mark - UIResponderStandardEditActions Overrides
1334
1335 - (void)cut:(id)sender {
1336 [UIPasteboard generalPasteboard].string = [self textInRange:_selectedTextRange];
1337 [self replaceRange:_selectedTextRange withText:@""];
1338 }
1339
1340 - (void)copy:(id)sender {
1341 [UIPasteboard generalPasteboard].string = [self textInRange:_selectedTextRange];
1342 }
1343
1344 - (void)paste:(id)sender {
1345 NSString* pasteboardString = [UIPasteboard generalPasteboard].string;
1346 if (pasteboardString != nil) {
1347 [self insertText:pasteboardString];
1348 }
1349 }
1350
1351 - (void)delete:(id)sender {
1352 [self replaceRange:_selectedTextRange withText:@""];
1353 }
1354
1355 - (void)selectAll:(id)sender {
1356 [self setSelectedTextRange:[self textRangeFromPosition:[self beginningOfDocument]
1357 toPosition:[self endOfDocument]]];
1358 }
1359
1360 #pragma mark - UITextInput Overrides
1361
1362 - (id<UITextInputTokenizer>)tokenizer {
1363 if (_tokenizer == nil) {
1364 _tokenizer = [[FlutterTokenizer alloc] initWithTextInput:self];
1365 }
1366 return _tokenizer;
1367 }
1368
1369 - (UITextRange*)selectedTextRange {
1370 return [_selectedTextRange copy];
1371 }
1372
1373 // Change the range of selected text, without notifying the framework.
1374 - (void)setSelectedTextRangeLocal:(UITextRange*)selectedTextRange {
1375 if (_selectedTextRange != selectedTextRange) {
1376 if (self.hasText) {
1377 FlutterTextRange* flutterTextRange = (FlutterTextRange*)selectedTextRange;
1378 _selectedTextRange = [[FlutterTextRange
1379 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, flutterTextRange.range)] copy];
1380 } else {
1381 _selectedTextRange = [selectedTextRange copy];
1382 }
1383 }
1384 }
1385
1386 - (void)setSelectedTextRange:(UITextRange*)selectedTextRange {
1387 if (!_enableInteractiveSelection) {
1388 return;
1389 }
1390
1391 [self setSelectedTextRangeLocal:selectedTextRange];
1392
1393 if (_enableDeltaModel) {
1394 [self updateEditingStateWithDelta:flutter::TextEditingDelta([self.text UTF8String])];
1395 } else {
1396 [self updateEditingState];
1397 }
1398
1399 if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone ||
1400 _scribbleFocusStatus == FlutterScribbleFocusStatusFocused) {
1401 NSAssert([selectedTextRange isKindOfClass:[FlutterTextRange class]],
1402 @"Expected a FlutterTextRange for range (got %@).", [selectedTextRange class]);
1403 FlutterTextRange* flutterTextRange = (FlutterTextRange*)selectedTextRange;
1404 if (flutterTextRange.range.length > 0) {
1405 [self.textInputDelegate flutterTextInputView:self showToolbar:_textInputClient];
1406 }
1407 }
1408
1409 [self resetScribbleInteractionStatusIfEnding];
1410 }
1411
1412 - (id)insertDictationResultPlaceholder {
1413 return @"";
1414 }
1415
1416 - (void)removeDictationResultPlaceholder:(id)placeholder willInsertResult:(BOOL)willInsertResult {
1417 }
1418
1419 - (NSString*)textInRange:(UITextRange*)range {
1420 if (!range) {
1421 return nil;
1422 }
1423 NSAssert([range isKindOfClass:[FlutterTextRange class]],
1424 @"Expected a FlutterTextRange for range (got %@).", [range class]);
1425 NSRange textRange = ((FlutterTextRange*)range).range;
1426 if (textRange.location == NSNotFound) {
1427 // Avoids [crashes](https://github.com/flutter/flutter/issues/138464) from an assertion
1428 // against NSNotFound.
1429 // TODO(hellohuanlin): This is a temp workaround, but we should look into why
1430 // framework is providing NSNotFound to the engine.
1431 // https://github.com/flutter/flutter/issues/160100
1432 return nil;
1433 }
1434 // Sanitize the range to prevent going out of bounds.
1435 NSUInteger location = MIN(textRange.location, self.text.length);
1436 NSUInteger length = MIN(self.text.length - location, textRange.length);
1437 NSRange safeRange = NSMakeRange(location, length);
1438 return [self.text substringWithRange:safeRange];
1439 }
1440
1441 // Replace the text within the specified range with the given text,
1442 // without notifying the framework.
1443 - (void)replaceRangeLocal:(NSRange)range withText:(NSString*)text {
1444 [self.text replaceCharactersInRange:[self clampSelection:range forText:self.text]
1445 withString:text];
1446
1447 // Adjust the selected range and the marked text range. There's no
1448 // documentation but UITextField always sets markedTextRange to nil,
1449 // and collapses the selection to the end of the new replacement text.
1450 const NSRange newSelectionRange =
1451 [self clampSelection:NSMakeRange(range.location + text.length, 0) forText:self.text];
1452
1453 [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:newSelectionRange]];
1454 self.markedTextRange = nil;
1455 }
1456
1457 - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
1458 NSString* textBeforeChange = [self.text copy];
1459 NSRange replaceRange = ((FlutterTextRange*)range).range;
1460 [self replaceRangeLocal:replaceRange withText:text];
1461 if (_enableDeltaModel) {
1462 NSRange nextReplaceRange = [self clampSelection:replaceRange forText:textBeforeChange];
1463 [self updateEditingStateWithDelta:flutter::TextEditingDelta(
1464 [textBeforeChange UTF8String],
1465 flutter::TextRange(
1466 nextReplaceRange.location,
1467 nextReplaceRange.location + nextReplaceRange.length),
1468 [text UTF8String])];
1469 } else {
1470 [self updateEditingState];
1471 }
1472 }
1473
1474 - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
1475 // `temporarilyDeletedComposedCharacter` should only be used during a single text change session.
1476 // So it needs to be cleared at the start of each text editing session.
1477 self.temporarilyDeletedComposedCharacter = nil;
1478
1479 if (self.returnKeyType == UIReturnKeyDefault && [text isEqualToString:@"\n"]) {
1480 [self.textInputDelegate flutterTextInputView:self
1481 performAction:FlutterTextInputActionNewline
1482 withClient:_textInputClient];
1483 return YES;
1484 }
1485
1486 if ([text isEqualToString:@"\n"]) {
1487 FlutterTextInputAction action;
1488 switch (self.returnKeyType) {
1489 case UIReturnKeyDefault:
1490 action = FlutterTextInputActionUnspecified;
1491 break;
1492 case UIReturnKeyDone:
1493 action = FlutterTextInputActionDone;
1494 break;
1495 case UIReturnKeyGo:
1496 action = FlutterTextInputActionGo;
1497 break;
1498 case UIReturnKeySend:
1499 action = FlutterTextInputActionSend;
1500 break;
1501 case UIReturnKeySearch:
1502 case UIReturnKeyGoogle:
1503 case UIReturnKeyYahoo:
1504 action = FlutterTextInputActionSearch;
1505 break;
1506 case UIReturnKeyNext:
1507 action = FlutterTextInputActionNext;
1508 break;
1509 case UIReturnKeyContinue:
1510 action = FlutterTextInputActionContinue;
1511 break;
1512 case UIReturnKeyJoin:
1513 action = FlutterTextInputActionJoin;
1514 break;
1515 case UIReturnKeyRoute:
1516 action = FlutterTextInputActionRoute;
1517 break;
1518 case UIReturnKeyEmergencyCall:
1519 action = FlutterTextInputActionEmergencyCall;
1520 break;
1521 }
1522
1523 [self.textInputDelegate flutterTextInputView:self
1524 performAction:action
1525 withClient:_textInputClient];
1526 return NO;
1527 }
1528
1529 return YES;
1530 }
1531
1532 // Either replaces the existing marked text or, if none is present, inserts it in
1533 // place of the current selection.
1534 - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelectedRange {
1535 NSString* textBeforeChange = [self.text copy];
1536
1537 if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone ||
1538 _scribbleFocusStatus != FlutterScribbleFocusStatusUnfocused) {
1539 return;
1540 }
1541
1542 if (markedText == nil) {
1543 markedText = @"";
1544 }
1545
1546 const FlutterTextRange* currentMarkedTextRange = (FlutterTextRange*)self.markedTextRange;
1547 const NSRange& actualReplacedRange = currentMarkedTextRange && !currentMarkedTextRange.isEmpty
1548 ? currentMarkedTextRange.range
1549 : _selectedTextRange.range;
1550 // No need to call replaceRangeLocal as this method always adjusts the
1551 // selected/marked text ranges anyways.
1552 [self.text replaceCharactersInRange:actualReplacedRange withString:markedText];
1553
1554 const NSRange newMarkedRange = NSMakeRange(actualReplacedRange.location, markedText.length);
1555 self.markedTextRange =
1556 newMarkedRange.length > 0 ? [FlutterTextRange rangeWithNSRange:newMarkedRange] : nil;
1557
1558 [self setSelectedTextRangeLocal:
1559 [FlutterTextRange
1560 rangeWithNSRange:[self clampSelection:NSMakeRange(markedSelectedRange.location +
1561 newMarkedRange.location,
1562 markedSelectedRange.length)
1563 forText:self.text]]];
1564 if (_enableDeltaModel) {
1565 NSRange nextReplaceRange = [self clampSelection:actualReplacedRange forText:textBeforeChange];
1566 [self updateEditingStateWithDelta:flutter::TextEditingDelta(
1567 [textBeforeChange UTF8String],
1568 flutter::TextRange(
1569 nextReplaceRange.location,
1570 nextReplaceRange.location + nextReplaceRange.length),
1571 [markedText UTF8String])];
1572 } else {
1573 [self updateEditingState];
1574 }
1575 }
1576
1577 - (void)unmarkText {
1578 if (!self.markedTextRange) {
1579 return;
1580 }
1581 self.markedTextRange = nil;
1582 if (_enableDeltaModel) {
1583 [self updateEditingStateWithDelta:flutter::TextEditingDelta([self.text UTF8String])];
1584 } else {
1585 [self updateEditingState];
1586 }
1587 }
1588
1589 - (UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition
1590 toPosition:(UITextPosition*)toPosition {
1591 NSUInteger fromIndex = ((FlutterTextPosition*)fromPosition).index;
1592 NSUInteger toIndex = ((FlutterTextPosition*)toPosition).index;
1593 if (toIndex >= fromIndex) {
1594 return [FlutterTextRange rangeWithNSRange:NSMakeRange(fromIndex, toIndex - fromIndex)];
1595 } else {
1596 // toIndex can be smaller than fromIndex, because
1597 // UITextInputStringTokenizer does not handle CJK characters
1598 // well in some cases. See:
1599 // https://github.com/flutter/flutter/issues/58750#issuecomment-644469521
1600 // Swap fromPosition and toPosition to match the behavior of native
1601 // UITextViews.
1602 return [FlutterTextRange rangeWithNSRange:NSMakeRange(toIndex, fromIndex - toIndex)];
1603 }
1604 }
1605
1606 - (NSUInteger)decrementOffsetPosition:(NSUInteger)position {
1607 return fml::RangeForCharacterAtIndex(self.text, MAX(0, position - 1)).location;
1608 }
1609
1610 - (NSUInteger)incrementOffsetPosition:(NSUInteger)position {
1611 NSRange charRange = fml::RangeForCharacterAtIndex(self.text, position);
1612 return MIN(position + charRange.length, self.text.length);
1613 }
1614
1615 - (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInteger)offset {
1616 NSUInteger offsetPosition = ((FlutterTextPosition*)position).index;
1617
1618 NSInteger newLocation = (NSInteger)offsetPosition + offset;
1619 if (newLocation < 0 || newLocation > (NSInteger)self.text.length) {
1620 return nil;
1621 }
1622
1623 if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone) {
1624 return [FlutterTextPosition positionWithIndex:newLocation];
1625 }
1626
1627 if (offset >= 0) {
1628 for (NSInteger i = 0; i < offset && offsetPosition < self.text.length; ++i) {
1629 offsetPosition = [self incrementOffsetPosition:offsetPosition];
1630 }
1631 } else {
1632 for (NSInteger i = 0; i < ABS(offset) && offsetPosition > 0; ++i) {
1633 offsetPosition = [self decrementOffsetPosition:offsetPosition];
1634 }
1635 }
1636 return [FlutterTextPosition positionWithIndex:offsetPosition];
1637 }
1638
1639 - (UITextPosition*)positionFromPosition:(UITextPosition*)position
1640 inDirection:(UITextLayoutDirection)direction
1641 offset:(NSInteger)offset {
1642 // TODO(cbracken) Add RTL handling.
1643 switch (direction) {
1644 case UITextLayoutDirectionLeft:
1645 case UITextLayoutDirectionUp:
1646 return [self positionFromPosition:position offset:offset * -1];
1647 case UITextLayoutDirectionRight:
1648 case UITextLayoutDirectionDown:
1649 return [self positionFromPosition:position offset:1];
1650 }
1651 }
1652
1653 - (UITextPosition*)beginningOfDocument {
1654 return [FlutterTextPosition positionWithIndex:0 affinity:UITextStorageDirectionForward];
1655 }
1656
1657 - (UITextPosition*)endOfDocument {
1658 return [FlutterTextPosition positionWithIndex:self.text.length
1659 affinity:UITextStorageDirectionBackward];
1660 }
1661
1662 - (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other {
1663 NSUInteger positionIndex = ((FlutterTextPosition*)position).index;
1664 NSUInteger otherIndex = ((FlutterTextPosition*)other).index;
1665 if (positionIndex < otherIndex) {
1666 return NSOrderedAscending;
1667 }
1668 if (positionIndex > otherIndex) {
1669 return NSOrderedDescending;
1670 }
1671 UITextStorageDirection positionAffinity = ((FlutterTextPosition*)position).affinity;
1672 UITextStorageDirection otherAffinity = ((FlutterTextPosition*)other).affinity;
1673 if (positionAffinity == otherAffinity) {
1674 return NSOrderedSame;
1675 }
1676 if (positionAffinity == UITextStorageDirectionBackward) {
1677 // positionAffinity points backwards, otherAffinity points forwards
1678 return NSOrderedAscending;
1679 }
1680 // positionAffinity points forwards, otherAffinity points backwards
1681 return NSOrderedDescending;
1682 }
1683
1684 - (NSInteger)offsetFromPosition:(UITextPosition*)from toPosition:(UITextPosition*)toPosition {
1685 return ((FlutterTextPosition*)toPosition).index - ((FlutterTextPosition*)from).index;
1686 }
1687
1688 - (UITextPosition*)positionWithinRange:(UITextRange*)range
1689 farthestInDirection:(UITextLayoutDirection)direction {
1690 NSUInteger index;
1691 UITextStorageDirection affinity;
1692 switch (direction) {
1693 case UITextLayoutDirectionLeft:
1694 case UITextLayoutDirectionUp:
1695 index = ((FlutterTextPosition*)range.start).index;
1696 affinity = UITextStorageDirectionForward;
1697 break;
1698 case UITextLayoutDirectionRight:
1699 case UITextLayoutDirectionDown:
1700 index = ((FlutterTextPosition*)range.end).index;
1701 affinity = UITextStorageDirectionBackward;
1702 break;
1703 }
1704 return [FlutterTextPosition positionWithIndex:index affinity:affinity];
1705 }
1706
1707 - (UITextRange*)characterRangeByExtendingPosition:(UITextPosition*)position
1708 inDirection:(UITextLayoutDirection)direction {
1709 NSUInteger positionIndex = ((FlutterTextPosition*)position).index;
1710 NSUInteger startIndex;
1711 NSUInteger endIndex;
1712 switch (direction) {
1713 case UITextLayoutDirectionLeft:
1714 case UITextLayoutDirectionUp:
1715 startIndex = [self decrementOffsetPosition:positionIndex];
1716 endIndex = positionIndex;
1717 break;
1718 case UITextLayoutDirectionRight:
1719 case UITextLayoutDirectionDown:
1720 startIndex = positionIndex;
1721 endIndex = [self incrementOffsetPosition:positionIndex];
1722 break;
1723 }
1724 return [FlutterTextRange rangeWithNSRange:NSMakeRange(startIndex, endIndex - startIndex)];
1725 }
1726
1727 #pragma mark - UITextInput text direction handling
1728
1729 - (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position
1730 inDirection:(UITextStorageDirection)direction {
1731 // TODO(cbracken) Add RTL handling.
1732 return UITextWritingDirectionNatural;
1733 }
1734
1735 - (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection
1736 forRange:(UITextRange*)range {
1737 // TODO(cbracken) Add RTL handling.
1738 }
1739
1740 #pragma mark - UITextInput cursor, selection rect handling
1741
1742 - (void)setMarkedRect:(CGRect)markedRect {
1743 _markedRect = markedRect;
1744 // Invalidate the cache.
1745 _cachedFirstRect = kInvalidFirstRect;
1746 }
1747
1748 // This method expects a 4x4 perspective matrix
1749 // stored in a NSArray in column-major order.
1750 - (void)setEditableTransform:(NSArray*)matrix {
1751 CATransform3D* transform = &_editableTransform;
1752
1753 transform->m11 = [matrix[0] doubleValue];
1754 transform->m12 = [matrix[1] doubleValue];
1755 transform->m13 = [matrix[2] doubleValue];
1756 transform->m14 = [matrix[3] doubleValue];
1757
1758 transform->m21 = [matrix[4] doubleValue];
1759 transform->m22 = [matrix[5] doubleValue];
1760 transform->m23 = [matrix[6] doubleValue];
1761 transform->m24 = [matrix[7] doubleValue];
1762
1763 transform->m31 = [matrix[8] doubleValue];
1764 transform->m32 = [matrix[9] doubleValue];
1765 transform->m33 = [matrix[10] doubleValue];
1766 transform->m34 = [matrix[11] doubleValue];
1767
1768 transform->m41 = [matrix[12] doubleValue];
1769 transform->m42 = [matrix[13] doubleValue];
1770 transform->m43 = [matrix[14] doubleValue];
1771 transform->m44 = [matrix[15] doubleValue];
1772
1773 // Invalidate the cache.
1774 _cachedFirstRect = kInvalidFirstRect;
1775 }
1776
1777 // Returns the bounding CGRect of the transformed incomingRect, in the view's
1778 // coordinates.
1779 - (CGRect)localRectFromFrameworkTransform:(CGRect)incomingRect {
1780 CGPoint points[] = {
1781 incomingRect.origin,
1782 CGPointMake(incomingRect.origin.x, incomingRect.origin.y + incomingRect.size.height),
1783 CGPointMake(incomingRect.origin.x + incomingRect.size.width, incomingRect.origin.y),
1784 CGPointMake(incomingRect.origin.x + incomingRect.size.width,
1785 incomingRect.origin.y + incomingRect.size.height)};
1786
1787 CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
1788 CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
1789
1790 for (int i = 0; i < 4; i++) {
1791 const CGPoint point = points[i];
1792
1793 CGFloat x = _editableTransform.m11 * point.x + _editableTransform.m21 * point.y +
1794 _editableTransform.m41;
1795 CGFloat y = _editableTransform.m12 * point.x + _editableTransform.m22 * point.y +
1796 _editableTransform.m42;
1797
1798 const CGFloat w = _editableTransform.m14 * point.x + _editableTransform.m24 * point.y +
1799 _editableTransform.m44;
1800
1801 if (w == 0.0) {
1802 return kInvalidFirstRect;
1803 } else if (w != 1.0) {
1804 x /= w;
1805 y /= w;
1806 }
1807
1808 origin.x = MIN(origin.x, x);
1809 origin.y = MIN(origin.y, y);
1810 farthest.x = MAX(farthest.x, x);
1811 farthest.y = MAX(farthest.y, y);
1812 }
1813 return CGRectMake(origin.x, origin.y, farthest.x - origin.x, farthest.y - origin.y);
1814 }
1815
1816 // The following methods are required to support force-touch cursor positioning
1817 // and to position the
1818 // candidates view for multi-stage input methods (e.g., Japanese) when using a
1819 // physical keyboard.
1820 // Returns the rect for the queried range, or a subrange through the end of line, if
1821 // the range encompasses multiple lines.
1822 - (CGRect)firstRectForRange:(UITextRange*)range {
1823 NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
1824 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
1825 NSAssert([range.end isKindOfClass:[FlutterTextPosition class]],
1826 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]);
1827 NSUInteger start = ((FlutterTextPosition*)range.start).index;
1828 NSUInteger end = ((FlutterTextPosition*)range.end).index;
1829 if (_markedTextRange != nil) {
1830 // The candidates view can't be shown if the framework has not sent the
1831 // first caret rect.
1832 if (CGRectEqualToRect(kInvalidFirstRect, _markedRect)) {
1833 return kInvalidFirstRect;
1834 }
1835
1836 if (CGRectEqualToRect(_cachedFirstRect, kInvalidFirstRect)) {
1837 // If the width returned is too small, that means the framework sent us
1838 // the caret rect instead of the marked text rect. Expand it to 0.2 so
1839 // the IME candidates view would show up.
1840 CGRect rect = _markedRect;
1841 if (CGRectIsEmpty(rect)) {
1842 rect = CGRectInset(rect, -0.1, 0);
1843 }
1844 _cachedFirstRect = [self localRectFromFrameworkTransform:rect];
1845 }
1846
1847 UIView* hostView = _textInputPlugin.hostView;
1848 NSAssert(hostView == nil || [self isDescendantOfView:hostView], @"%@ is not a descendant of %@",
1849 self, hostView);
1850 return hostView ? [hostView convertRect:_cachedFirstRect toView:self] : _cachedFirstRect;
1851 }
1852
1853 if (_scribbleInteractionStatus == FlutterScribbleInteractionStatusNone &&
1854 _scribbleFocusStatus == FlutterScribbleFocusStatusUnfocused) {
1855 if (@available(iOS 17.0, *)) {
1856 // Disable auto-correction highlight feature for iOS 17+.
1857 // In iOS 17+, whenever a character is inserted or deleted, the system will always query
1858 // the rect for every single character of the current word.
1859 // GitHub Issue: https://github.com/flutter/flutter/issues/128406
1860 } else {
1861 // This tells the framework to show the highlight for incorrectly spelled word that is
1862 // about to be auto-corrected.
1863 // There is no other UITextInput API that informs about the auto-correction highlight.
1864 // So we simply add the call here as a workaround.
1865 [self.textInputDelegate flutterTextInputView:self
1866 showAutocorrectionPromptRectForStart:start
1867 end:end
1868 withClient:_textInputClient];
1869 }
1870 }
1871
1872 // The iOS 16 system highlight does not repect the height returned by `firstRectForRange`
1873 // API (unlike iOS 17). So we return CGRectZero to hide it (unless if scribble is enabled).
1874 // To support scribble's advanced gestures (e.g. insert a space with a vertical bar),
1875 // at least 1 character's width is required.
1876 if (@available(iOS 17, *)) {
1877 // No-op
1878 } else if (![self isScribbleAvailable]) {
1879 return CGRectZero;
1880 }
1881
1882 NSUInteger first = start;
1883 if (end < start) {
1884 first = end;
1885 }
1886
1887 CGRect startSelectionRect = CGRectNull;
1888 CGRect endSelectionRect = CGRectNull;
1889 // Selection rects from different langauges may have different minY/maxY.
1890 // So we need to iterate through each rects to update minY/maxY.
1891 CGFloat minY = CGFLOAT_MAX;
1892 CGFloat maxY = CGFLOAT_MIN;
1893
1894 FlutterTextRange* textRange = [FlutterTextRange
1895 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1896 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1897 BOOL startsOnOrBeforeStartOfRange = _selectionRects[i].position <= first;
1898 BOOL isLastSelectionRect = i + 1 == [_selectionRects count];
1899 BOOL endOfTextIsAfterStartOfRange = isLastSelectionRect && textRange.range.length > first;
1900 BOOL nextSelectionRectIsAfterStartOfRange =
1901 !isLastSelectionRect && _selectionRects[i + 1].position > first;
1902 if (startsOnOrBeforeStartOfRange &&
1903 (endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
1904 // TODO(hellohaunlin): Remove iOS 17 check. The logic should also work for older versions.
1905 if (@available(iOS 17, *)) {
1906 startSelectionRect = _selectionRects[i].rect;
1907 } else {
1908 return _selectionRects[i].rect;
1909 }
1910 }
1911 if (!CGRectIsNull(startSelectionRect)) {
1912 minY = fmin(minY, CGRectGetMinY(_selectionRects[i].rect));
1913 maxY = fmax(maxY, CGRectGetMaxY(_selectionRects[i].rect));
1914 BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1; // end is exclusive
1915 BOOL nextSelectionRectIsOnNextLine =
1916 !isLastSelectionRect &&
1917 // Selection rects from different langauges in 2 lines may overlap with each other.
1918 // A good approximation is to check if the center of next rect is below the bottom of
1919 // current rect.
1920 // TODO(hellohuanlin): Consider passing the line break info from framework.
1921 CGRectGetMidY(_selectionRects[i + 1].rect) > CGRectGetMaxY(_selectionRects[i].rect);
1922 if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectionRectIsOnNextLine) {
1923 endSelectionRect = _selectionRects[i].rect;
1924 break;
1925 }
1926 }
1927 }
1928 if (CGRectIsNull(startSelectionRect) || CGRectIsNull(endSelectionRect)) {
1929 return CGRectZero;
1930 } else {
1931 // fmin/fmax to support both LTR and RTL languages.
1932 CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
1933 CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
1934 return CGRectMake(minX, minY, maxX - minX, maxY - minY);
1935 }
1936 }
1937
1938 - (CGRect)caretRectForPosition:(UITextPosition*)position {
1939 NSInteger index = ((FlutterTextPosition*)position).index;
1940 UITextStorageDirection affinity = ((FlutterTextPosition*)position).affinity;
1941 // Get the selectionRect of the characters before and after the requested caret position.
1942 NSArray<UITextSelectionRect*>* rects = [self
1943 selectionRectsForRange:[FlutterTextRange
1944 rangeWithNSRange:fml::RangeForCharactersInRange(
1945 self.text,
1946 NSMakeRange(
1947 MAX(0, index - 1),
1948 (index >= (NSInteger)self.text.length)
1949 ? 1
1950 : 2))]];
1951 if (rects.count == 0) {
1952 return CGRectZero;
1953 }
1954 if (index == 0) {
1955 // There is no character before the caret, so this will be the bounds of the character after the
1956 // caret position.
1957 CGRect characterAfterCaret = rects[0].rect;
1958 // Return a zero-width rectangle along the upstream edge of the character after the caret
1959 // position.
1960 if ([rects[0] isKindOfClass:[FlutterTextSelectionRect class]] &&
1961 ((FlutterTextSelectionRect*)rects[0]).isRTL) {
1962 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1963 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1964 } else {
1965 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1966 characterAfterCaret.size.height);
1967 }
1968 } else if (rects.count == 2 && affinity == UITextStorageDirectionForward) {
1969 // There are characters before and after the caret, with forward direction affinity.
1970 // It's better to use the character after the caret.
1971 CGRect characterAfterCaret = rects[1].rect;
1972 // Return a zero-width rectangle along the upstream edge of the character after the caret
1973 // position.
1974 if ([rects[1] isKindOfClass:[FlutterTextSelectionRect class]] &&
1975 ((FlutterTextSelectionRect*)rects[1]).isRTL) {
1976 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1977 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1978 } else {
1979 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1980 characterAfterCaret.size.height);
1981 }
1982 }
1983
1984 // Covers 2 remaining cases:
1985 // 1. there are characters before and after the caret, with backward direction affinity.
1986 // 2. there is only 1 character before the caret (caret is at the end of text).
1987 // For both cases, return a zero-width rectangle along the downstream edge of the character
1988 // before the caret position.
1989 CGRect characterBeforeCaret = rects[0].rect;
1990 if ([rects[0] isKindOfClass:[FlutterTextSelectionRect class]] &&
1991 ((FlutterTextSelectionRect*)rects[0]).isRTL) {
1992 return CGRectMake(characterBeforeCaret.origin.x, characterBeforeCaret.origin.y, 0,
1993 characterBeforeCaret.size.height);
1994 } else {
1995 return CGRectMake(characterBeforeCaret.origin.x + characterBeforeCaret.size.width,
1996 characterBeforeCaret.origin.y, 0, characterBeforeCaret.size.height);
1997 }
1998 }
1999
2000 - (UITextPosition*)closestPositionToPoint:(CGPoint)point {
2001 if ([_selectionRects count] == 0) {
2002 NSAssert([_selectedTextRange.start isKindOfClass:[FlutterTextPosition class]],
2003 @"Expected a FlutterTextPosition for position (got %@).",
2004 [_selectedTextRange.start class]);
2005 NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index;
2006 UITextStorageDirection currentAffinity =
2007 ((FlutterTextPosition*)_selectedTextRange.start).affinity;
2008 return [FlutterTextPosition positionWithIndex:currentIndex affinity:currentAffinity];
2009 }
2010
2011 FlutterTextRange* range = [FlutterTextRange
2012 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
2013 return [self closestPositionToPoint:point withinRange:range];
2014 }
2015
2016 - (NSArray*)selectionRectsForRange:(UITextRange*)range {
2017 // At least in the simulator, swapping to the Japanese keyboard crashes the app as this method
2018 // is called immediately with a UITextRange with a UITextPosition rather than FlutterTextPosition
2019 // for the start and end.
2020 if (![range.start isKindOfClass:[FlutterTextPosition class]]) {
2021 return @[];
2022 }
2023 NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
2024 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
2025 NSAssert([range.end isKindOfClass:[FlutterTextPosition class]],
2026 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]);
2027 NSUInteger start = ((FlutterTextPosition*)range.start).index;
2028 NSUInteger end = ((FlutterTextPosition*)range.end).index;
2029 NSMutableArray* rects = [[NSMutableArray alloc] init];
2030 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2031 if (_selectionRects[i].position >= start &&
2032 (_selectionRects[i].position < end ||
2033 (start == end && _selectionRects[i].position <= end))) {
2034 float width = _selectionRects[i].rect.size.width;
2035 if (start == end) {
2036 width = 0;
2037 }
2038 CGRect rect = CGRectMake(_selectionRects[i].rect.origin.x, _selectionRects[i].rect.origin.y,
2039 width, _selectionRects[i].rect.size.height);
2040 FlutterTextSelectionRect* selectionRect = [FlutterTextSelectionRect
2041 selectionRectWithRectAndInfo:rect
2042 position:_selectionRects[i].position
2043 writingDirection:NSWritingDirectionNatural
2044 containsStart:(i == 0)
2045 containsEnd:(i == fml::RangeForCharactersInRange(
2046 self.text, NSMakeRange(0, self.text.length))
2047 .length)
2048 isVertical:NO];
2049 [rects addObject:selectionRect];
2050 }
2051 }
2052 return rects;
2053 }
2054
2055 - (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range {
2056 NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
2057 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
2058 NSAssert([range.end isKindOfClass:[FlutterTextPosition class]],
2059 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]);
2060 NSUInteger start = ((FlutterTextPosition*)range.start).index;
2061 NSUInteger end = ((FlutterTextPosition*)range.end).index;
2062
2063 // Selecting text using the floating cursor is not as precise as the pencil.
2064 // Allow further vertical deviation and base more of the decision on horizontal comparison.
2065 CGFloat verticalPrecision = _isFloatingCursorActive ? 10 : 1;
2066
2067 // Find the selectionRect with a leading-center point that is closest to a given point.
2068 BOOL isFirst = YES;
2069 NSUInteger _closestRectIndex = 0;
2070 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2071 NSUInteger position = _selectionRects[i].position;
2072 if (position >= start && position <= end) {
2073 if (isFirst ||
2074 IsSelectionRectBoundaryCloserToPoint(
2075 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
2076 /*useTrailingBoundaryOfSelectionRect=*/NO, _selectionRects[_closestRectIndex].rect,
2077 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
2078 isFirst = NO;
2079 _closestRectIndex = i;
2080 }
2081 }
2082 }
2083
2084 FlutterTextPosition* closestPosition =
2085 [FlutterTextPosition positionWithIndex:_selectionRects[_closestRectIndex].position
2086 affinity:UITextStorageDirectionForward];
2087
2088 // Check if the far side of the closest rect is a better fit (e.g. tapping end of line)
2089 // Cannot simply check the _closestRectIndex result from the previous for loop due to RTL
2090 // writing direction and the gaps between selectionRects. So we also need to consider
2091 // the adjacent selectionRects to refine _closestRectIndex.
2092 for (NSUInteger i = MAX(0, _closestRectIndex - 1);
2093 i < MIN(_closestRectIndex + 2, [_selectionRects count]); i++) {
2094 NSUInteger position = _selectionRects[i].position + 1;
2095 if (position >= start && position <= end) {
2096 if (IsSelectionRectBoundaryCloserToPoint(
2097 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
2098 /*useTrailingBoundaryOfSelectionRect=*/YES, _selectionRects[_closestRectIndex].rect,
2099 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
2100 // This is an upstream position
2101 closestPosition = [FlutterTextPosition positionWithIndex:position
2102 affinity:UITextStorageDirectionBackward];
2103 }
2104 }
2105 }
2106
2107 return closestPosition;
2108 }
2109
2110 - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
2111 // TODO(cbracken) Implement.
2112 NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index;
2113 return [FlutterTextRange rangeWithNSRange:fml::RangeForCharacterAtIndex(self.text, currentIndex)];
2114 }
2115
2116 // Overall logic for floating cursor's "move" gesture and "selection" gesture:
2117 //
2118 // Floating cursor's "move" gesture takes 1 finger to force press the space bar, and then move the
2119 // cursor. The process starts with `beginFloatingCursorAtPoint`. When the finger is moved,
2120 // `updateFloatingCursorAtPoint` will be called. When the finger is released, `endFloatingCursor`
2121 // will be called. In all cases, we send the point (relative to the initial point registered in
2122 // beginFloatingCursorAtPoint) to the framework, so that framework can animate the floating cursor.
2123 //
2124 // During the move gesture, the framework only animate the cursor visually. It's only
2125 // after the gesture is complete, will the framework update the selection to the cursor's
2126 // new position (with zero selection length). This means during the animation, the visual effect
2127 // of the cursor is temporarily out of sync with the selection state in both framework and engine.
2128 // But it will be in sync again after the animation is complete.
2129 //
2130 // Floating cursor's "selection" gesture also starts with 1 finger to force press the space bar,
2131 // so exactly the same functions as the "move gesture" discussed above will be called. When the
2132 // second finger is pressed, `setSelectedText` will be called. This mechanism requires
2133 // `closestPositionFromPoint` to be implemented, to allow UIKit to translate the finger touch
2134 // location displacement to the text range to select. When the selection is completed
2135 // (i.e. when both of the 2 fingers are released), similar to "move" gesture,
2136 // the `endFloatingCursor` will be called.
2137 //
2138 // When the 2nd finger is pressed, it does not trigger another startFloatingCursor call. So
2139 // floating cursor move/selection logic has to be implemented in iOS embedder rather than
2140 // just the framework side.
2141 //
2142 // Whenever a selection is updated, the engine sends the new selection to the framework. So unlike
2143 // the move gesture, the selections in the framework and the engine are always kept in sync.
2144 - (void)beginFloatingCursorAtPoint:(CGPoint)point {
2145 // For "beginFloatingCursorAtPoint" and "updateFloatingCursorAtPoint", "point" is roughly:
2146 //
2147 // CGPoint(
2148 // width >= 0 ? point.x.clamp(boundingBox.left, boundingBox.right) : point.x,
2149 // height >= 0 ? point.y.clamp(boundingBox.top, boundingBox.bottom) : point.y,
2150 // )
2151 // where
2152 // point = keyboardPanGestureRecognizer.translationInView(textInputView) + caretRectForPosition
2153 // boundingBox = self.convertRect(bounds, fromView:textInputView)
2154 // bounds = self._selectionClipRect ?? self.bounds
2155 //
2156 // It seems impossible to use a negative "width" or "height", as the "convertRect"
2157 // call always turns a CGRect's negative dimensions into non-negative values, e.g.,
2158 // (1, 2, -3, -4) would become (-2, -2, 3, 4).
2159 _isFloatingCursorActive = YES;
2160 _floatingCursorOffset = point;
2161 [self.textInputDelegate flutterTextInputView:self
2162 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2163 withClient:_textInputClient
2164 withPosition:@{@"X" : @0, @"Y" : @0}];
2165 }
2166
2167 - (void)updateFloatingCursorAtPoint:(CGPoint)point {
2168 [self.textInputDelegate flutterTextInputView:self
2169 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2170 withClient:_textInputClient
2171 withPosition:@{
2172 @"X" : @(point.x - _floatingCursorOffset.x),
2173 @"Y" : @(point.y - _floatingCursorOffset.y)
2174 }];
2175 }
2176
2177 - (void)endFloatingCursor {
2178 _isFloatingCursorActive = NO;
2179 [self.textInputDelegate flutterTextInputView:self
2180 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2181 withClient:_textInputClient
2182 withPosition:@{@"X" : @0, @"Y" : @0}];
2183 }
2184
2185 #pragma mark - UIKeyInput Overrides
2186
2187 - (void)updateEditingState {
2188 NSUInteger selectionBase = ((FlutterTextPosition*)_selectedTextRange.start).index;
2189 NSUInteger selectionExtent = ((FlutterTextPosition*)_selectedTextRange.end).index;
2190
2191 // Empty compositing range is represented by the framework's TextRange.empty.
2192 NSInteger composingBase = -1;
2193 NSInteger composingExtent = -1;
2194 if (self.markedTextRange != nil) {
2195 composingBase = ((FlutterTextPosition*)self.markedTextRange.start).index;
2196 composingExtent = ((FlutterTextPosition*)self.markedTextRange.end).index;
2197 }
2198 NSDictionary* state = @{
2199 @"selectionBase" : @(selectionBase),
2200 @"selectionExtent" : @(selectionExtent),
2201 @"selectionAffinity" : @(_selectionAffinity),
2202 @"selectionIsDirectional" : @(false),
2203 @"composingBase" : @(composingBase),
2204 @"composingExtent" : @(composingExtent),
2205 @"text" : [NSString stringWithString:self.text],
2206 };
2207
2208 if (_textInputClient == 0 && _autofillId != nil) {
2209 [self.textInputDelegate flutterTextInputView:self
2210 updateEditingClient:_textInputClient
2211 withState:state
2212 withTag:_autofillId];
2213 } else {
2214 [self.textInputDelegate flutterTextInputView:self
2215 updateEditingClient:_textInputClient
2216 withState:state];
2217 }
2218 }
2219
2220 - (void)updateEditingStateWithDelta:(flutter::TextEditingDelta)delta {
2221 NSUInteger selectionBase = ((FlutterTextPosition*)_selectedTextRange.start).index;
2222 NSUInteger selectionExtent = ((FlutterTextPosition*)_selectedTextRange.end).index;
2223
2224 // Empty compositing range is represented by the framework's TextRange.empty.
2225 NSInteger composingBase = -1;
2226 NSInteger composingExtent = -1;
2227 if (self.markedTextRange != nil) {
2228 composingBase = ((FlutterTextPosition*)self.markedTextRange.start).index;
2229 composingExtent = ((FlutterTextPosition*)self.markedTextRange.end).index;
2230 }
2231
2232 NSDictionary* deltaToFramework = @{
2233 @"oldText" : @(delta.old_text().c_str()),
2234 @"deltaText" : @(delta.delta_text().c_str()),
2235 @"deltaStart" : @(delta.delta_start()),
2236 @"deltaEnd" : @(delta.delta_end()),
2237 @"selectionBase" : @(selectionBase),
2238 @"selectionExtent" : @(selectionExtent),
2239 @"selectionAffinity" : @(_selectionAffinity),
2240 @"selectionIsDirectional" : @(false),
2241 @"composingBase" : @(composingBase),
2242 @"composingExtent" : @(composingExtent),
2243 };
2244
2245 [_pendingDeltas addObject:deltaToFramework];
2246
2247 if (_pendingDeltas.count == 1) {
2248 __weak FlutterTextInputView* weakSelf = self;
2249 dispatch_async(dispatch_get_main_queue(), ^{
2250 __strong FlutterTextInputView* strongSelf = weakSelf;
2251 if (strongSelf && strongSelf.pendingDeltas.count > 0) {
2252 NSDictionary* deltas = @{
2253 @"deltas" : strongSelf.pendingDeltas,
2254 };
2255
2256 [strongSelf.textInputDelegate flutterTextInputView:strongSelf
2257 updateEditingClient:strongSelf->_textInputClient
2258 withDelta:deltas];
2259 [strongSelf.pendingDeltas removeAllObjects];
2260 }
2261 });
2262 }
2263 }
2264
2265 - (BOOL)hasText {
2266 return self.text.length > 0;
2267 }
2268
2269 - (void)insertText:(NSString*)text {
2270 if (self.temporarilyDeletedComposedCharacter.length > 0 && text.length == 1 && !text.UTF8String &&
2271 [text characterAtIndex:0] == [self.temporarilyDeletedComposedCharacter characterAtIndex:0]) {
2272 // Workaround for https://github.com/flutter/flutter/issues/111494
2273 // TODO(cyanglaz): revert this workaround if when flutter supports a minimum iOS version which
2274 // this bug is fixed by Apple.
2275 text = self.temporarilyDeletedComposedCharacter;
2276 self.temporarilyDeletedComposedCharacter = nil;
2277 }
2278
2279 NSMutableArray<FlutterTextSelectionRect*>* copiedRects =
2280 [[NSMutableArray alloc] initWithCapacity:[_selectionRects count]];
2281 NSAssert([_selectedTextRange.start isKindOfClass:[FlutterTextPosition class]],
2282 @"Expected a FlutterTextPosition for position (got %@).",
2283 [_selectedTextRange.start class]);
2284 NSUInteger insertPosition = ((FlutterTextPosition*)_selectedTextRange.start).index;
2285 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2286 NSUInteger rectPosition = _selectionRects[i].position;
2287 if (rectPosition == insertPosition) {
2288 for (NSUInteger j = 0; j <= text.length; j++) {
2289 [copiedRects addObject:[FlutterTextSelectionRect
2290 selectionRectWithRect:_selectionRects[i].rect
2291 position:rectPosition + j
2292 writingDirection:_selectionRects[i].writingDirection]];
2293 }
2294 } else {
2295 if (rectPosition > insertPosition) {
2296 rectPosition = rectPosition + text.length;
2297 }
2298 [copiedRects addObject:[FlutterTextSelectionRect
2299 selectionRectWithRect:_selectionRects[i].rect
2300 position:rectPosition
2301 writingDirection:_selectionRects[i].writingDirection]];
2302 }
2303 }
2304
2305 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2306 [self resetScribbleInteractionStatusIfEnding];
2307 self.selectionRects = copiedRects;
2308 _selectionAffinity = kTextAffinityDownstream;
2309 [self replaceRange:_selectedTextRange withText:text];
2310 }
2311
2312 - (UITextPlaceholder*)insertTextPlaceholderWithSize:(CGSize)size API_AVAILABLE(ios(13.0)) {
2313 [self.textInputDelegate flutterTextInputView:self
2314 insertTextPlaceholderWithSize:size
2315 withClient:_textInputClient];
2316 _hasPlaceholder = YES;
2317 return [[FlutterTextPlaceholder alloc] init];
2318 }
2319
2320 - (void)removeTextPlaceholder:(UITextPlaceholder*)textPlaceholder API_AVAILABLE(ios(13.0)) {
2321 _hasPlaceholder = NO;
2322 [self.textInputDelegate flutterTextInputView:self removeTextPlaceholder:_textInputClient];
2323 }
2324
2325 - (void)deleteBackward {
2326 _selectionAffinity = kTextAffinityDownstream;
2327 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2328 [self resetScribbleInteractionStatusIfEnding];
2329
2330 // When deleting Thai vowel, _selectedTextRange has location
2331 // but does not have length, so we have to manually set it.
2332 // In addition, we needed to delete only a part of grapheme cluster
2333 // because it is the expected behavior of Thai input.
2334 // https://github.com/flutter/flutter/issues/24203
2335 // https://github.com/flutter/flutter/issues/21745
2336 // https://github.com/flutter/flutter/issues/39399
2337 //
2338 // This is needed for correct handling of the deletion of Thai vowel input.
2339 // TODO(cbracken): Get a good understanding of expected behavior of Thai
2340 // input and ensure that this is the correct solution.
2341 // https://github.com/flutter/flutter/issues/28962
2342 if (_selectedTextRange.isEmpty && [self hasText]) {
2343 UITextRange* oldSelectedRange = _selectedTextRange;
2344 NSRange oldRange = ((FlutterTextRange*)oldSelectedRange).range;
2345 if (oldRange.location > 0) {
2346 NSRange newRange = NSMakeRange(oldRange.location - 1, 1);
2347
2348 // We should check if the last character is a part of emoji.
2349 // If so, we must delete the entire emoji to prevent the text from being malformed.
2350 NSRange charRange = fml::RangeForCharacterAtIndex(self.text, oldRange.location - 1);
2351 if (IsEmoji(self.text, charRange)) {
2352 newRange = NSMakeRange(charRange.location, oldRange.location - charRange.location);
2353 }
2354
2355 _selectedTextRange = [[FlutterTextRange rangeWithNSRange:newRange] copy];
2356 }
2357 }
2358
2359 if (!_selectedTextRange.isEmpty) {
2360 // Cache the last deleted emoji to use for an iOS bug where the next
2361 // insertion corrupts the emoji characters.
2362 // See: https://github.com/flutter/flutter/issues/111494#issuecomment-1248441346
2363 if (IsEmoji(self.text, _selectedTextRange.range)) {
2364 NSString* deletedText = [self.text substringWithRange:_selectedTextRange.range];
2365 NSRange deleteFirstCharacterRange = fml::RangeForCharacterAtIndex(deletedText, 0);
2366 self.temporarilyDeletedComposedCharacter =
2367 [deletedText substringWithRange:deleteFirstCharacterRange];
2368 }
2369 [self replaceRange:_selectedTextRange withText:@""];
2370 }
2371 }
2372
2373 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(id)target {
2374 UIAccessibilityPostNotification(notification, target);
2375 }
2376
2377 - (void)accessibilityElementDidBecomeFocused {
2378 if ([self accessibilityElementIsFocused]) {
2379 // For most of the cases, this flutter text input view should never
2380 // receive the focus. If we do receive the focus, we make the best effort
2381 // to send the focus back to the real text field.
2382 FML_DCHECK(_backingTextInputAccessibilityObject);
2383 [self postAccessibilityNotification:UIAccessibilityScreenChangedNotification
2384 target:_backingTextInputAccessibilityObject];
2385 }
2386 }
2387
2388 - (BOOL)accessibilityElementsHidden {
2389 return !_accessibilityEnabled;
2390 }
2391
2392 - (void)resetScribbleInteractionStatusIfEnding {
2393 if (_scribbleInteractionStatus == FlutterScribbleInteractionStatusEnding) {
2394 _scribbleInteractionStatus = FlutterScribbleInteractionStatusNone;
2395 }
2396 }
2397
2398 #pragma mark - Key Events Handling
2399 - (void)pressesBegan:(NSSet<UIPress*>*)presses
2400 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2401 [_textInputPlugin.viewController pressesBegan:presses withEvent:event];
2402 }
2403
2404 - (void)pressesChanged:(NSSet<UIPress*>*)presses
2405 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2406 [_textInputPlugin.viewController pressesChanged:presses withEvent:event];
2407 }
2408
2409 - (void)pressesEnded:(NSSet<UIPress*>*)presses
2410 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2411 [_textInputPlugin.viewController pressesEnded:presses withEvent:event];
2412 }
2413
2414 - (void)pressesCancelled:(NSSet<UIPress*>*)presses
2415 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2416 [_textInputPlugin.viewController pressesCancelled:presses withEvent:event];
2417 }
2418
2419 @end
2420
2421 /**
2422 * Hides `FlutterTextInputView` from iOS accessibility system so it
2423 * does not show up twice, once where it is in the `UIView` hierarchy,
2424 * and a second time as part of the `SemanticsObject` hierarchy.
2425 *
2426 * This prevents the `FlutterTextInputView` from receiving the focus
2427 * due to swiping gesture.
2428 *
2429 * There are other cases the `FlutterTextInputView` may receive
2430 * focus. One example is during screen changes, the accessibility
2431 * tree will undergo a dramatic structural update. The Voiceover may
2432 * decide to focus the `FlutterTextInputView` that is not involved
2433 * in the structural update instead. If that happens, the
2434 * `FlutterTextInputView` will make a best effort to direct the
2435 * focus back to the `SemanticsObject`.
2436 */
2437 @interface FlutterTextInputViewAccessibilityHider : UIView {
2438 }
2439
2440 @end
2441
2442 @implementation FlutterTextInputViewAccessibilityHider {
2443 }
2444
2445 - (BOOL)accessibilityElementsHidden {
2446 return YES;
2447 }
2448
2449 @end
2450
2451 @interface FlutterTextInputPlugin ()
2452 - (void)enableActiveViewAccessibility;
2453 @end
2454
2455 @interface FlutterTimerProxy : NSObject
2456 @property(nonatomic, weak) FlutterTextInputPlugin* target;
2457 @end
2458
2459 @implementation FlutterTimerProxy
2460
2461 + (instancetype)proxyWithTarget:(FlutterTextInputPlugin*)target {
2462 FlutterTimerProxy* proxy = [[self alloc] init];
2463 if (proxy) {
2464 proxy.target = target;
2465 }
2466 return proxy;
2467 }
2468
2469 - (void)enableActiveViewAccessibility {
2470 [self.target enableActiveViewAccessibility];
2471 }
2472
2473 @end
2474
2475 @interface FlutterTextInputPlugin ()
2476 // The current password-autofillable input fields that have yet to be saved.
2477 @property(nonatomic, readonly)
2478 NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
2479 @property(nonatomic, retain) FlutterTextInputView* activeView;
2480 @property(nonatomic, retain) FlutterTextInputViewAccessibilityHider* inputHider;
2481 @property(nonatomic, readonly, weak) id<FlutterViewResponder> viewResponder;
2482
2483 @property(nonatomic, strong) UIView* keyboardViewContainer;
2484 @property(nonatomic, strong) UIView* keyboardView;
2485 @property(nonatomic, strong) UIView* cachedFirstResponder;
2486 @property(nonatomic, assign) CGRect keyboardRect;
2487 @property(nonatomic, assign) CGFloat previousPointerYPosition;
2488 @property(nonatomic, assign) CGFloat pointerYVelocity;
2489 @end
2490
2491 @implementation FlutterTextInputPlugin {
2492 NSTimer* _enableFlutterTextInputViewAccessibilityTimer;
2493 }
2494
2495 - (instancetype)initWithDelegate:(id<FlutterTextInputDelegate>)textInputDelegate {
2496 self = [super init];
2497 if (self) {
2498 // `_textInputDelegate` is a weak reference because it should retain FlutterTextInputPlugin.
2499 _textInputDelegate = textInputDelegate;
2500 _autofillContext = [[NSMutableDictionary alloc] init];
2501 _inputHider = [[FlutterTextInputViewAccessibilityHider alloc] init];
2502 _scribbleElements = [[NSMutableDictionary alloc] init];
2503 _keyboardViewContainer = [[UIView alloc] init];
2504
2505 [[NSNotificationCenter defaultCenter] addObserver:self
2506 selector:@selector(handleKeyboardWillShow:)
2507 name:UIKeyboardWillShowNotification
2508 object:nil];
2509 }
2510
2511 return self;
2512 }
2513
2514 - (void)handleKeyboardWillShow:(NSNotification*)notification {
2515 NSDictionary* keyboardInfo = [notification userInfo];
2516 NSValue* keyboardFrameEnd = [keyboardInfo valueForKey:UIKeyboardFrameEndUserInfoKey];
2517 _keyboardRect = [keyboardFrameEnd CGRectValue];
2518 }
2519
2520 - (void)dealloc {
2521 [self hideTextInput];
2522 }
2523
2524 - (void)removeEnableFlutterTextInputViewAccessibilityTimer {
2525 if (_enableFlutterTextInputViewAccessibilityTimer) {
2526 [_enableFlutterTextInputViewAccessibilityTimer invalidate];
2527 _enableFlutterTextInputViewAccessibilityTimer = nil;
2528 }
2529 }
2530
2531 - (UIView<UITextInput>*)textInputView {
2532 return _activeView;
2533 }
2534
2535 - (void)reset {
2536 [self hideTextInput];
2537 }
2538
2539 - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
2540 NSString* method = call.method;
2541 id args = call.arguments;
2542 if ([method isEqualToString:kShowMethod]) {
2543 [self showTextInput];
2544 result(nil);
2545 } else if ([method isEqualToString:kHideMethod]) {
2546 [self hideTextInput];
2547 result(nil);
2548 } else if ([method isEqualToString:kSetClientMethod]) {
2549 [self setTextInputClient:[args[0] intValue] withConfiguration:args[1]];
2550 result(nil);
2551 } else if ([method isEqualToString:kSetPlatformViewClientMethod]) {
2552 // This method call has a `platformViewId` argument, but we do not need it for iOS for now.
2553 [self setPlatformViewTextInputClient];
2554 result(nil);
2555 } else if ([method isEqualToString:kSetEditingStateMethod]) {
2556 [self setTextInputEditingState:args];
2557 result(nil);
2558 } else if ([method isEqualToString:kClearClientMethod]) {
2559 [self clearTextInputClient];
2560 result(nil);
2561 } else if ([method isEqualToString:kSetEditableSizeAndTransformMethod]) {
2562 [self setEditableSizeAndTransform:args];
2563 result(nil);
2564 } else if ([method isEqualToString:kSetMarkedTextRectMethod]) {
2565 [self updateMarkedRect:args];
2566 result(nil);
2567 } else if ([method isEqualToString:kFinishAutofillContextMethod]) {
2568 [self triggerAutofillSave:[args boolValue]];
2569 result(nil);
2570 // TODO(justinmc): Remove the TextInput method constant when the framework has
2571 // finished transitioning to using the Scribble channel.
2572 // https://github.com/flutter/flutter/pull/104128
2573 } else if ([method isEqualToString:kDeprecatedSetSelectionRectsMethod]) {
2574 [self setSelectionRects:args];
2575 result(nil);
2576 } else if ([method isEqualToString:kSetSelectionRectsMethod]) {
2577 [self setSelectionRects:args];
2578 result(nil);
2579 } else if ([method isEqualToString:kStartLiveTextInputMethod]) {
2580 [self startLiveTextInput];
2581 result(nil);
2582 } else if ([method isEqualToString:kUpdateConfigMethod]) {
2583 [self updateConfig:args];
2584 result(nil);
2585 } else if ([method isEqualToString:kOnInteractiveKeyboardPointerMoveMethod]) {
2586 CGFloat pointerY = (CGFloat)[args[@"pointerY"] doubleValue];
2587 [self handlePointerMove:pointerY];
2588 result(nil);
2589 } else if ([method isEqualToString:kOnInteractiveKeyboardPointerUpMethod]) {
2590 CGFloat pointerY = (CGFloat)[args[@"pointerY"] doubleValue];
2591 [self handlePointerUp:pointerY];
2592 result(nil);
2593 } else {
2594 result(FlutterMethodNotImplemented);
2595 }
2596 }
2597
2598 - (void)handlePointerUp:(CGFloat)pointerY {
2599 if (_keyboardView.superview != nil) {
2600 // Done to avoid the issue of a pointer up done without a screenshot
2601 // View must be loaded at this point.
2602 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2603 CGFloat screenHeight = screen.bounds.size.height;
2604 CGFloat keyboardHeight = _keyboardRect.size.height;
2605 // Negative velocity indicates a downward movement
2606 BOOL shouldDismissKeyboardBasedOnVelocity = _pointerYVelocity < 0;
2607 [UIView animateWithDuration:kKeyboardAnimationTimeToCompleteion
2608 animations:^{
2609 double keyboardDestination =
2610 shouldDismissKeyboardBasedOnVelocity ? screenHeight : screenHeight - keyboardHeight;
2611 _keyboardViewContainer.frame = CGRectMake(
2612 0, keyboardDestination, _viewController.flutterScreenIfViewLoaded.bounds.size.width,
2613 _keyboardViewContainer.frame.size.height);
2614 }
2615 completion:^(BOOL finished) {
2616 if (shouldDismissKeyboardBasedOnVelocity) {
2617 [self.textInputDelegate flutterTextInputView:self.activeView
2618 didResignFirstResponderWithTextInputClient:self.activeView.textInputClient];
2619 [self dismissKeyboardScreenshot];
2620 } else {
2621 [self showKeyboardAndRemoveScreenshot];
2622 }
2623 }];
2624 }
2625 }
2626
2627 - (void)dismissKeyboardScreenshot {
2628 for (UIView* subView in _keyboardViewContainer.subviews) {
2629 [subView removeFromSuperview];
2630 }
2631 }
2632
2633 - (void)showKeyboardAndRemoveScreenshot {
2634 [UIView setAnimationsEnabled:NO];
2635 [_cachedFirstResponder becomeFirstResponder];
2636 // UIKit does not immediately access the areAnimationsEnabled Boolean so a delay is needed before
2637 // returned
2638 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kKeyboardAnimationDelaySeconds * NSEC_PER_SEC),
2639 dispatch_get_main_queue(), ^{
2640 [UIView setAnimationsEnabled:YES];
2641 [self dismissKeyboardScreenshot];
2642 });
2643 }
2644
2645 - (void)handlePointerMove:(CGFloat)pointerY {
2646 // View must be loaded at this point.
2647 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2648 CGFloat screenHeight = screen.bounds.size.height;
2649 CGFloat keyboardHeight = _keyboardRect.size.height;
2650 if (screenHeight - keyboardHeight <= pointerY) {
2651 // If the pointer is within the bounds of the keyboard.
2652 if (_keyboardView.superview == nil) {
2653 // If no screenshot has been taken.
2654 [self takeKeyboardScreenshotAndDisplay];
2655 [self hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate];
2656 } else {
2657 [self setKeyboardContainerHeight:pointerY];
2658 _pointerYVelocity = _previousPointerYPosition - pointerY;
2659 }
2660 } else {
2661 if (_keyboardView.superview != nil) {
2662 // Keeps keyboard at proper height.
2663 _keyboardViewContainer.frame = _keyboardRect;
2664 _pointerYVelocity = _previousPointerYPosition - pointerY;
2665 }
2666 }
2667 _previousPointerYPosition = pointerY;
2668 }
2669
2670 - (void)setKeyboardContainerHeight:(CGFloat)pointerY {
2671 CGRect frameRect = _keyboardRect;
2672 frameRect.origin.y = pointerY;
2673 _keyboardViewContainer.frame = frameRect;
2674 }
2675
2676 - (void)hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate {
2677 [UIView setAnimationsEnabled:NO];
2678 UIApplication* flutterApplication = FlutterSharedApplication.application;
2679 _cachedFirstResponder =
2680 flutterApplication
2681 ? flutterApplication.keyWindow.flutterFirstResponder
2682 : self.viewController.flutterWindowSceneIfViewLoaded.keyWindow.flutterFirstResponder;
2683
2684 _activeView.preventCursorDismissWhenResignFirstResponder = YES;
2685 [_cachedFirstResponder resignFirstResponder];
2686 _activeView.preventCursorDismissWhenResignFirstResponder = NO;
2687 [UIView setAnimationsEnabled:YES];
2688 }
2689
2690 - (void)takeKeyboardScreenshotAndDisplay {
2691 // View must be loaded at this point
2692 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2693 UIView* keyboardSnap = [screen snapshotViewAfterScreenUpdates:YES];
2694 keyboardSnap = [keyboardSnap resizableSnapshotViewFromRect:_keyboardRect
2695 afterScreenUpdates:YES
2696 withCapInsets:UIEdgeInsetsZero];
2697 _keyboardView = keyboardSnap;
2698 [_keyboardViewContainer addSubview:_keyboardView];
2699 if (_keyboardViewContainer.superview == nil) {
2700 UIApplication* flutterApplication = FlutterSharedApplication.application;
2701 UIView* rootView = flutterApplication
2702 ? flutterApplication.delegate.window.rootViewController.view
2703 : self.viewController.viewIfLoaded.window.rootViewController.view;
2704 [rootView addSubview:_keyboardViewContainer];
2705 }
2706 _keyboardViewContainer.layer.zPosition = NSIntegerMax;
2707 _keyboardViewContainer.frame = _keyboardRect;
2708 }
2709
2710 - (BOOL)showEditMenu:(NSDictionary*)args API_AVAILABLE(ios(16.0)) {
2711 if (!self.activeView.isFirstResponder) {
2712 return NO;
2713 }
2714 NSDictionary<NSString*, NSNumber*>* encodedTargetRect = args[@"targetRect"];
2715 CGRect globalTargetRect = CGRectMake(
2716 [encodedTargetRect[@"x"] doubleValue], [encodedTargetRect[@"y"] doubleValue],
2717 [encodedTargetRect[@"width"] doubleValue], [encodedTargetRect[@"height"] doubleValue]);
2718 CGRect localTargetRect = [self.hostView convertRect:globalTargetRect toView:self.activeView];
2719 [self.activeView showEditMenuWithTargetRect:localTargetRect items:args[@"items"]];
2720 return YES;
2721 }
2722
2723 - (void)hideEditMenu {
2724 [self.activeView hideEditMenu];
2725 }
2726
2727 - (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
2728 NSArray* transform = dictionary[@"transform"];
2729 [_activeView setEditableTransform:transform];
2730 const int leftIndex = 12;
2731 const int topIndex = 13;
2732 if ([_activeView isScribbleAvailable]) {
2733 // This is necessary to set up where the scribble interactable element will be.
2734 _inputHider.frame =
2735 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue],
2736 [dictionary[@"width"] intValue], [dictionary[@"height"] intValue]);
2737 _activeView.frame =
2738 CGRectMake(0, 0, [dictionary[@"width"] intValue], [dictionary[@"height"] intValue]);
2739 _activeView.tintColor = [UIColor clearColor];
2740 } else {
2741 // TODO(hellohuanlin): Also need to handle iOS 16 case, where the auto-correction highlight does
2742 // not match the size of text.
2743 // See https://github.com/flutter/flutter/issues/131695
2744 if (@available(iOS 17, *)) {
2745 // Move auto-correction highlight to overlap with the actual text.
2746 // This is to fix an issue where the system auto-correction highlight is displayed at
2747 // the top left corner of the screen on iOS 17+.
2748 // This problem also happens on iOS 16, but the size of highlight does not match the text.
2749 // See https://github.com/flutter/flutter/issues/131695
2750 // TODO(hellohuanlin): Investigate if we can use non-zero size.
2751 _inputHider.frame =
2752 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue], 0, 0);
2753 }
2754 }
2755 }
2756
2757 - (void)updateMarkedRect:(NSDictionary*)dictionary {
2758 NSAssert(dictionary[@"x"] != nil && dictionary[@"y"] != nil && dictionary[@"width"] != nil &&
2759 dictionary[@"height"] != nil,
2760 @"Expected a dictionary representing a CGRect, got %@", dictionary);
2761 CGRect rect = CGRectMake([dictionary[@"x"] doubleValue], [dictionary[@"y"] doubleValue],
2762 [dictionary[@"width"] doubleValue], [dictionary[@"height"] doubleValue]);
2763 _activeView.markedRect = rect.size.width < 0 && rect.size.height < 0 ? kInvalidFirstRect : rect;
2764 }
2765
2766 - (void)setSelectionRects:(NSArray*)encodedRects {
2767 NSMutableArray<FlutterTextSelectionRect*>* rectsAsRect =
2768 [[NSMutableArray alloc] initWithCapacity:[encodedRects count]];
2769 for (NSUInteger i = 0; i < [encodedRects count]; i++) {
2770 NSArray<NSNumber*>* encodedRect = encodedRects[i];
2771 [rectsAsRect addObject:[FlutterTextSelectionRect
2772 selectionRectWithRect:CGRectMake([encodedRect[0] floatValue],
2773 [encodedRect[1] floatValue],
2774 [encodedRect[2] floatValue],
2775 [encodedRect[3] floatValue])
2776 position:[encodedRect[4] unsignedIntegerValue]
2777 writingDirection:[encodedRect[5] unsignedIntegerValue] == 1
2778 ? NSWritingDirectionLeftToRight
2779 : NSWritingDirectionRightToLeft]];
2780 }
2781
2782 // TODO(hellohuanlin): Investigate why notifying the text input system about text changes (via
2783 // textWillChange and textDidChange APIs) causes a bug where we cannot enter text with IME
2784 // keyboards. Issue: https://github.com/flutter/flutter/issues/133908
2785 _activeView.selectionRects = rectsAsRect;
2786 }
2787
2788 - (void)startLiveTextInput {
2789 if (@available(iOS 15.0, *)) {
2790 if (_activeView == nil || !_activeView.isFirstResponder) {
2791 return;
2792 }
2793 [_activeView captureTextFromCamera:nil];
2794 }
2795 }
2796
2797 - (void)showTextInput {
2798 _activeView.viewResponder = _viewResponder;
2799 [self addToInputParentViewIfNeeded:_activeView];
2800 // Adds a delay to prevent the text view from receiving accessibility
2801 // focus in case it is activated during semantics updates.
2802 //
2803 // One common case is when the app navigates to a page with an auto
2804 // focused text field. The text field will activate the FlutterTextInputView
2805 // with a semantics update sent to the engine. The voiceover will focus
2806 // the newly attached active view while performing accessibility update.
2807 // This results in accessibility focus stuck at the FlutterTextInputView.
2808 if (!_enableFlutterTextInputViewAccessibilityTimer) {
2809 _enableFlutterTextInputViewAccessibilityTimer =
2810 [NSTimer scheduledTimerWithTimeInterval:kUITextInputAccessibilityEnablingDelaySeconds
2811 target:[FlutterTimerProxy proxyWithTarget:self]
2812 selector:@selector(enableActiveViewAccessibility)
2813 userInfo:nil
2814 repeats:NO];
2815 }
2816 [_activeView becomeFirstResponder];
2817 }
2818
2819 - (void)enableActiveViewAccessibility {
2820 if (_activeView.isFirstResponder) {
2821 _activeView.accessibilityEnabled = YES;
2822 }
2823 [self removeEnableFlutterTextInputViewAccessibilityTimer];
2824 }
2825
2826 - (void)hideTextInput {
2827 [self removeEnableFlutterTextInputViewAccessibilityTimer];
2828 _activeView.accessibilityEnabled = NO;
2829 [_activeView resignFirstResponder];
2830 [_activeView removeFromSuperview];
2831 [_inputHider removeFromSuperview];
2832 }
2833
2834 - (void)triggerAutofillSave:(BOOL)saveEntries {
2835 [_activeView resignFirstResponder];
2836
2837 if (saveEntries) {
2838 // Make all the input fields in the autofill context visible,
2839 // then remove them to trigger autofill save.
2840 [self cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
2841 [_autofillContext removeAllObjects];
2842 [self changeInputViewsAutofillVisibility:YES];
2843 } else {
2844 [_autofillContext removeAllObjects];
2845 }
2846
2847 [self cleanUpViewHierarchy:YES clearText:!saveEntries delayRemoval:NO];
2848 [self addToInputParentViewIfNeeded:_activeView];
2849 }
2850
2851 - (void)setPlatformViewTextInputClient {
2852 // No need to track the platformViewID (unlike in Android). When a platform view
2853 // becomes the first responder, simply hide this dummy text input view (`_activeView`)
2854 // for the previously focused widget.
2855 [self removeEnableFlutterTextInputViewAccessibilityTimer];
2856 _activeView.accessibilityEnabled = NO;
2857 [_activeView removeFromSuperview];
2858 [_inputHider removeFromSuperview];
2859 }
2860
2861 - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configuration {
2862 [self resetAllClientIds];
2863 // Hide all input views from autofill, only make those in the new configuration visible
2864 // to autofill.
2865 [self changeInputViewsAutofillVisibility:NO];
2866
2867 // Update the current active view.
2868 switch (AutofillTypeOf(configuration)) {
2869 case kFlutterAutofillTypeNone:
2870 self.activeView = [self createInputViewWith:configuration];
2871 break;
2872 case kFlutterAutofillTypeRegular:
2873 // If the group does not involve password autofill, only install the
2874 // input view that's being focused.
2875 self.activeView = [self updateAndShowAutofillViews:nil
2876 focusedField:configuration
2877 isPasswordRelated:NO];
2878 break;
2879 case kFlutterAutofillTypePassword:
2880 self.activeView = [self updateAndShowAutofillViews:configuration[kAssociatedAutofillFields]
2881 focusedField:configuration
2882 isPasswordRelated:YES];
2883 break;
2884 }
2885 [_activeView setTextInputClient:client];
2886 [_activeView reloadInputViews];
2887
2888 // Clean up views that no longer need to be in the view hierarchy, according to
2889 // the current autofill context. The "garbage" input views are already made
2890 // invisible to autofill and they can't `becomeFirstResponder`, we only remove
2891 // them to free up resources and reduce the number of input views in the view
2892 // hierarchy.
2893 //
2894 // The garbage views are decommissioned immediately, but the removeFromSuperview
2895 // call is scheduled on the runloop and delayed by 0.1s so we don't remove the
2896 // text fields immediately (which seems to make the keyboard flicker).
2897 // See: https://github.com/flutter/flutter/issues/64628.
2898 [self cleanUpViewHierarchy:NO clearText:YES delayRemoval:YES];
2899 }
2900
2901 // Creates and shows an input field that is not password related and has no autofill
2902 // info. This method returns a new FlutterTextInputView instance when called, since
2903 // UIKit uses the identity of `UITextInput` instances (or the identity of the input
2904 // views) to decide whether the IME's internal states should be reset. See:
2905 // https://github.com/flutter/flutter/issues/79031 .
2906 - (FlutterTextInputView*)createInputViewWith:(NSDictionary*)configuration {
2907 NSString* autofillId = AutofillIdFromDictionary(configuration);
2908 if (autofillId) {
2909 [_autofillContext removeObjectForKey:autofillId];
2910 }
2911 FlutterTextInputView* newView = [[FlutterTextInputView alloc] initWithOwner:self];
2912 [newView configureWithDictionary:configuration];
2913 [self addToInputParentViewIfNeeded:newView];
2914
2915 for (NSDictionary* field in configuration[kAssociatedAutofillFields]) {
2916 NSString* autofillId = AutofillIdFromDictionary(field);
2917 if (autofillId && AutofillTypeOf(field) == kFlutterAutofillTypeNone) {
2918 [_autofillContext removeObjectForKey:autofillId];
2919 }
2920 }
2921 return newView;
2922 }
2923
2924 - (FlutterTextInputView*)updateAndShowAutofillViews:(NSArray*)fields
2925 focusedField:(NSDictionary*)focusedField
2926 isPasswordRelated:(BOOL)isPassword {
2927 FlutterTextInputView* focused = nil;
2928 NSString* focusedId = AutofillIdFromDictionary(focusedField);
2929 NSAssert(focusedId, @"autofillId must not be null for the focused field: %@", focusedField);
2930
2931 if (!fields) {
2932 // DO NOT push the current autofillable input fields to the context even
2933 // if it's password-related, because it is not in an autofill group.
2934 focused = [self getOrCreateAutofillableView:focusedField isPasswordAutofill:isPassword];
2935 [_autofillContext removeObjectForKey:focusedId];
2936 }
2937
2938 for (NSDictionary* field in fields) {
2939 NSString* autofillId = AutofillIdFromDictionary(field);
2940 NSAssert(autofillId, @"autofillId must not be null for field: %@", field);
2941
2942 BOOL hasHints = AutofillTypeOf(field) != kFlutterAutofillTypeNone;
2943 BOOL isFocused = [focusedId isEqualToString:autofillId];
2944
2945 if (isFocused) {
2946 focused = [self getOrCreateAutofillableView:field isPasswordAutofill:isPassword];
2947 }
2948
2949 if (hasHints) {
2950 // Push the current input field to the context if it has hints.
2951 _autofillContext[autofillId] = isFocused ? focused
2952 : [self getOrCreateAutofillableView:field
2953 isPasswordAutofill:isPassword];
2954 } else {
2955 // Mark for deletion.
2956 [_autofillContext removeObjectForKey:autofillId];
2957 }
2958 }
2959
2960 NSAssert(focused, @"The current focused input view must not be nil.");
2961 return focused;
2962 }
2963
2964 // Returns a new non-reusable input view (and put it into the view hierarchy), or get the
2965 // view from the current autofill context, if an input view with the same autofill id
2966 // already exists in the context.
2967 // This is generally used for input fields that are autofillable (UIKit tracks these veiws
2968 // for autofill purposes so they should not be reused for a different type of views).
2969 - (FlutterTextInputView*)getOrCreateAutofillableView:(NSDictionary*)field
2970 isPasswordAutofill:(BOOL)needsPasswordAutofill {
2971 NSString* autofillId = AutofillIdFromDictionary(field);
2972 FlutterTextInputView* inputView = _autofillContext[autofillId];
2973 if (!inputView) {
2974 inputView =
2975 needsPasswordAutofill ? [FlutterSecureTextInputView alloc] : [FlutterTextInputView alloc];
2976 inputView = [inputView initWithOwner:self];
2977 [self addToInputParentViewIfNeeded:inputView];
2978 }
2979
2980 [inputView configureWithDictionary:field];
2981 return inputView;
2982 }
2983
2984 // The UIView to add FlutterTextInputViews to.
2985 - (UIView*)hostView {
2986 UIView* host = _viewController.view;
2987 NSAssert(host != nullptr,
2988 @"The application must have a host view since the keyboard client "
2989 @"must be part of the responder chain to function. The host view controller is %@",
2990 _viewController);
2991 return host;
2992 }
2993
2994 // The UIView to add FlutterTextInputViews to.
2995 - (NSArray<UIView*>*)textInputViews {
2996 return _inputHider.subviews;
2997 }
2998
2999 // Removes every installed input field, unless it's in the current autofill context.
3000 //
3001 // The active view will be removed from its superview too, if includeActiveView is YES.
3002 // When clearText is YES, the text on the input fields will be set to empty before
3003 // they are removed from the view hierarchy, to avoid triggering autofill save.
3004 // If delayRemoval is true, removeFromSuperview will be scheduled on the runloop and
3005 // will be delayed by 0.1s so we don't remove the text fields immediately (which seems
3006 // to make the keyboard flicker).
3007 // See: https://github.com/flutter/flutter/issues/64628.
3008
3009 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
3010 clearText:(BOOL)clearText
3011 delayRemoval:(BOOL)delayRemoval {
3012 for (UIView* view in self.textInputViews) {
3013 if ([view isKindOfClass:[FlutterTextInputView class]] &&
3014 (includeActiveView || view != _activeView)) {
3015 FlutterTextInputView* inputView = (FlutterTextInputView*)view;
3016 if (_autofillContext[inputView.autofillId] != view) {
3017 if (clearText) {
3018 [inputView replaceRangeLocal:NSMakeRange(0, inputView.text.length) withText:@""];
3019 }
3020 if (delayRemoval) {
3021 [inputView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:0.1];
3022 } else {
3023 [inputView removeFromSuperview];
3024 }
3025 }
3026 }
3027 }
3028 }
3029
3030 // Changes the visibility of every FlutterTextInputView currently in the
3031 // view hierarchy.
3032 - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
3033 for (UIView* view in self.textInputViews) {
3034 if ([view isKindOfClass:[FlutterTextInputView class]]) {
3035 FlutterTextInputView* inputView = (FlutterTextInputView*)view;
3036 inputView.isVisibleToAutofill = newVisibility;
3037 }
3038 }
3039 }
3040
3041 // Resets the client id of every FlutterTextInputView in the view hierarchy
3042 // to 0.
3043 // Called before establishing a new text input connection.
3044 // For views in the current autofill context, they need to
3045 // stay in the view hierachy but should not be allowed to
3046 // send messages (other than autofill related ones) to the
3047 // framework.
3048 - (void)resetAllClientIds {
3049 for (UIView* view in self.textInputViews) {
3050 if ([view isKindOfClass:[FlutterTextInputView class]]) {
3051 FlutterTextInputView* inputView = (FlutterTextInputView*)view;
3052 [inputView setTextInputClient:0];
3053 }
3054 }
3055 }
3056
3057 - (void)addToInputParentViewIfNeeded:(FlutterTextInputView*)inputView {
3058 if (![inputView isDescendantOfView:_inputHider]) {
3059 [_inputHider addSubview:inputView];
3060 }
3061
3062 if (_viewController.view == nil) {
3063 // If view controller's view has detached from flutter engine, we don't add _inputHider
3064 // in parent view to fallback and avoid crash.
3065 // https://github.com/flutter/flutter/issues/106404.
3066 return;
3067 }
3068 UIView* parentView = self.hostView;
3069 if (_inputHider.superview != parentView) {
3070 [parentView addSubview:_inputHider];
3071 }
3072 }
3073
3074 - (void)setTextInputEditingState:(NSDictionary*)state {
3075 [_activeView setTextInputState:state];
3076 }
3077
3078 - (void)clearTextInputClient {
3079 [_activeView setTextInputClient:0];
3080 _activeView.frame = CGRectZero;
3081 }
3082
3083 - (void)updateConfig:(NSDictionary*)dictionary {
3084 BOOL isSecureTextEntry = [dictionary[kSecureTextEntry] boolValue];
3085 for (UIView* view in self.textInputViews) {
3086 if ([view isKindOfClass:[FlutterTextInputView class]]) {
3087 FlutterTextInputView* inputView = (FlutterTextInputView*)view;
3088 // The feature of holding and draging spacebar to move cursor is affected by
3089 // secureTextEntry, so when obscureText is updated, we need to update secureTextEntry
3090 // and call reloadInputViews.
3091 // https://github.com/flutter/flutter/issues/122139
3092 if (inputView.isSecureTextEntry != isSecureTextEntry) {
3093 inputView.secureTextEntry = isSecureTextEntry;
3094 [inputView reloadInputViews];
3095 }
3096 }
3097 }
3098 }
3099
3100 #pragma mark UIIndirectScribbleInteractionDelegate
3101
3102 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3103 isElementFocused:(UIScribbleElementIdentifier)elementIdentifier
3104 API_AVAILABLE(ios(14.0)) {
3105 return _activeView.scribbleFocusStatus == FlutterScribbleFocusStatusFocused;
3106 }
3107
3108 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3109 focusElementIfNeeded:(UIScribbleElementIdentifier)elementIdentifier
3110 referencePoint:(CGPoint)focusReferencePoint
3111 completion:(void (^)(UIResponder<UITextInput>* focusedInput))completion
3112 API_AVAILABLE(ios(14.0)) {
3113 _activeView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
3114 [_indirectScribbleDelegate flutterTextInputPlugin:self
3115 focusElement:elementIdentifier
3116 atPoint:focusReferencePoint
3117 result:^(id _Nullable result) {
3118 _activeView.scribbleFocusStatus =
3119 FlutterScribbleFocusStatusFocused;
3120 completion(_activeView);
3121 }];
3122 }
3123
3124 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3125 shouldDelayFocusForElement:(UIScribbleElementIdentifier)elementIdentifier
3126 API_AVAILABLE(ios(14.0)) {
3127 return NO;
3128 }
3129
3130 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3131 willBeginWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
3132 API_AVAILABLE(ios(14.0)) {
3133 }
3134
3135 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3136 didFinishWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
3137 API_AVAILABLE(ios(14.0)) {
3138 }
3139
3140 - (CGRect)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3141 frameForElement:(UIScribbleElementIdentifier)elementIdentifier
3142 API_AVAILABLE(ios(14.0)) {
3143 NSValue* elementValue = [_scribbleElements objectForKey:elementIdentifier];
3144 if (elementValue == nil) {
3145 return CGRectZero;
3146 }
3147 return [elementValue CGRectValue];
3148 }
3149
3150 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3151 requestElementsInRect:(CGRect)rect
3152 completion:
3153 (void (^)(NSArray<UIScribbleElementIdentifier>* elements))completion
3154 API_AVAILABLE(ios(14.0)) {
3155 [_indirectScribbleDelegate
3156 flutterTextInputPlugin:self
3157 requestElementsInRect:rect
3158 result:^(id _Nullable result) {
3159 NSMutableArray<UIScribbleElementIdentifier>* elements =
3160 [[NSMutableArray alloc] init];
3161 if ([result isKindOfClass:[NSArray class]]) {
3162 for (NSArray* elementArray in result) {
3163 [elements addObject:elementArray[0]];
3164 [_scribbleElements
3165 setObject:[NSValue
3166 valueWithCGRect:CGRectMake(
3167 [elementArray[1] floatValue],
3168 [elementArray[2] floatValue],
3169 [elementArray[3] floatValue],
3170 [elementArray[4] floatValue])]
3171 forKey:elementArray[0]];
3172 }
3173 }
3174 completion(elements);
3175 }];
3176 }
3177
3178 #pragma mark - Methods related to Scribble support
3179
3180 - (void)setUpIndirectScribbleInteraction:(id<FlutterViewResponder>)viewResponder {
3181 if (_viewResponder != viewResponder) {
3182 if (@available(iOS 14.0, *)) {
3183 UIView* parentView = viewResponder.view;
3184 if (parentView != nil) {
3185 UIIndirectScribbleInteraction* scribbleInteraction = [[UIIndirectScribbleInteraction alloc]
3186 initWithDelegate:(id<UIIndirectScribbleInteractionDelegate>)self];
3187 [parentView addInteraction:scribbleInteraction];
3188 }
3189 }
3190 }
3191 _viewResponder = viewResponder;
3192 }
3193
3194 - (void)resetViewResponder {
3195 _viewResponder = nil;
3196 }
3197
3198 #pragma mark -
3199 #pragma mark FlutterKeySecondaryResponder
3200
3201 /**
3202 * Handles key down events received from the view controller, responding YES if
3203 * the event was handled.
3204 */
3205 - (BOOL)handlePress:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4)) {
3206 return NO;
3207 }
3208 @end
3209
3210 /**
3211 * Recursively searches the UIView's subviews to locate the First Responder
3212 */
3213 @implementation UIView (FindFirstResponder)
3214 - (id)flutterFirstResponder {
3215 if (self.isFirstResponder) {
3216 return self;
3217 }
3218 for (UIView* subView in self.subviews) {
3219 UIView* firstResponder = subView.flutterFirstResponder;
3220 if (firstResponder) {
3221 return firstResponder;
3222 }
3223 }
3224 return nil;
3225 }
3226 @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:11
FlutterSharedApplication::application
UIApplication * application
Definition: FlutterSharedApplication.h:25
FlutterTextInputPlugin
Definition: FlutterTextInputPlugin.h:34
FlutterTextInputViewAccessibilityHider
Definition: FlutterTextInputPlugin.mm:2437
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:2456
FlutterTimerProxy::target
FlutterTextInputPlugin * target
Definition: FlutterTextInputPlugin.mm:2456
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