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