8 #import <Foundation/Foundation.h>
9 #import <UIKit/UIKit.h>
11 #include "unicode/uchar.h"
13 #include "flutter/fml/logging.h"
14 #include "flutter/fml/platform/darwin/string_range_sanitization.h"
37 #pragma mark - TextInput channel method names.
46 @"TextInput.setEditableSizeAndTransform";
57 @"TextInput.onPointerMoveForInteractiveKeyboard";
59 @"TextInput.onPointerUpForInteractiveKeyboard";
61 #pragma mark - TextInputConfiguration Field Names
82 #pragma mark - Static Functions
85 static BOOL
IsEmoji(NSString* text, NSRange charRange) {
87 BOOL gotCodePoint = [text getBytes:&codePoint
88 maxLength:
sizeof(codePoint)
90 encoding:NSUTF32StringEncoding
94 return gotCodePoint && u_hasBinaryProperty(codePoint, UCHAR_EMOJI);
102 NSString* inputType = type[
@"name"];
103 return ![inputType isEqualToString:
@"TextInputType.none"];
106 NSString* inputType = type[
@"name"];
107 if ([inputType isEqualToString:
@"TextInputType.address"]) {
108 return UIKeyboardTypeDefault;
110 if ([inputType isEqualToString:
@"TextInputType.datetime"]) {
111 return UIKeyboardTypeNumbersAndPunctuation;
113 if ([inputType isEqualToString:
@"TextInputType.emailAddress"]) {
114 return UIKeyboardTypeEmailAddress;
116 if ([inputType isEqualToString:
@"TextInputType.multiline"]) {
117 return UIKeyboardTypeDefault;
119 if ([inputType isEqualToString:
@"TextInputType.name"]) {
120 return UIKeyboardTypeNamePhonePad;
122 if ([inputType isEqualToString:
@"TextInputType.number"]) {
123 if ([type[
@"signed"] boolValue]) {
124 return UIKeyboardTypeNumbersAndPunctuation;
126 if ([type[
@"decimal"] boolValue]) {
127 return UIKeyboardTypeDecimalPad;
129 return UIKeyboardTypeNumberPad;
131 if ([inputType isEqualToString:
@"TextInputType.phone"]) {
132 return UIKeyboardTypePhonePad;
134 if ([inputType isEqualToString:
@"TextInputType.text"]) {
135 return UIKeyboardTypeDefault;
137 if ([inputType isEqualToString:
@"TextInputType.url"]) {
138 return UIKeyboardTypeURL;
140 return UIKeyboardTypeDefault;
144 NSString* textCapitalization = type[
@"textCapitalization"];
145 if ([textCapitalization isEqualToString:
@"TextCapitalization.characters"]) {
146 return UITextAutocapitalizationTypeAllCharacters;
147 }
else if ([textCapitalization isEqualToString:
@"TextCapitalization.sentences"]) {
148 return UITextAutocapitalizationTypeSentences;
149 }
else if ([textCapitalization isEqualToString:
@"TextCapitalization.words"]) {
150 return UITextAutocapitalizationTypeWords;
152 return UITextAutocapitalizationTypeNone;
160 if ([inputType isEqualToString:
@"TextInputAction.unspecified"]) {
161 return UIReturnKeyDefault;
164 if ([inputType isEqualToString:
@"TextInputAction.done"]) {
165 return UIReturnKeyDone;
168 if ([inputType isEqualToString:
@"TextInputAction.go"]) {
169 return UIReturnKeyGo;
172 if ([inputType isEqualToString:
@"TextInputAction.send"]) {
173 return UIReturnKeySend;
176 if ([inputType isEqualToString:
@"TextInputAction.search"]) {
177 return UIReturnKeySearch;
180 if ([inputType isEqualToString:
@"TextInputAction.next"]) {
181 return UIReturnKeyNext;
184 if ([inputType isEqualToString:
@"TextInputAction.continueAction"]) {
185 return UIReturnKeyContinue;
188 if ([inputType isEqualToString:
@"TextInputAction.join"]) {
189 return UIReturnKeyJoin;
192 if ([inputType isEqualToString:
@"TextInputAction.route"]) {
193 return UIReturnKeyRoute;
196 if ([inputType isEqualToString:
@"TextInputAction.emergencyCall"]) {
197 return UIReturnKeyEmergencyCall;
200 if ([inputType isEqualToString:
@"TextInputAction.newline"]) {
201 return UIReturnKeyDefault;
205 return UIReturnKeyDefault;
209 if (!hints || hints.count == 0) {
214 NSString* hint = hints[0];
215 if ([hint isEqualToString:
@"addressCityAndState"]) {
216 return UITextContentTypeAddressCityAndState;
219 if ([hint isEqualToString:
@"addressState"]) {
220 return UITextContentTypeAddressState;
223 if ([hint isEqualToString:
@"addressCity"]) {
224 return UITextContentTypeAddressCity;
227 if ([hint isEqualToString:
@"sublocality"]) {
228 return UITextContentTypeSublocality;
231 if ([hint isEqualToString:
@"streetAddressLine1"]) {
232 return UITextContentTypeStreetAddressLine1;
235 if ([hint isEqualToString:
@"streetAddressLine2"]) {
236 return UITextContentTypeStreetAddressLine2;
239 if ([hint isEqualToString:
@"countryName"]) {
240 return UITextContentTypeCountryName;
243 if ([hint isEqualToString:
@"fullStreetAddress"]) {
244 return UITextContentTypeFullStreetAddress;
247 if ([hint isEqualToString:
@"postalCode"]) {
248 return UITextContentTypePostalCode;
251 if ([hint isEqualToString:
@"location"]) {
252 return UITextContentTypeLocation;
255 if ([hint isEqualToString:
@"creditCardNumber"]) {
256 return UITextContentTypeCreditCardNumber;
259 if ([hint isEqualToString:
@"email"]) {
260 return UITextContentTypeEmailAddress;
263 if ([hint isEqualToString:
@"jobTitle"]) {
264 return UITextContentTypeJobTitle;
267 if ([hint isEqualToString:
@"givenName"]) {
268 return UITextContentTypeGivenName;
271 if ([hint isEqualToString:
@"middleName"]) {
272 return UITextContentTypeMiddleName;
275 if ([hint isEqualToString:
@"familyName"]) {
276 return UITextContentTypeFamilyName;
279 if ([hint isEqualToString:
@"name"]) {
280 return UITextContentTypeName;
283 if ([hint isEqualToString:
@"namePrefix"]) {
284 return UITextContentTypeNamePrefix;
287 if ([hint isEqualToString:
@"nameSuffix"]) {
288 return UITextContentTypeNameSuffix;
291 if ([hint isEqualToString:
@"nickname"]) {
292 return UITextContentTypeNickname;
295 if ([hint isEqualToString:
@"organizationName"]) {
296 return UITextContentTypeOrganizationName;
299 if ([hint isEqualToString:
@"telephoneNumber"]) {
300 return UITextContentTypeTelephoneNumber;
303 if ([hint isEqualToString:
@"password"]) {
304 return UITextContentTypePassword;
307 if (@available(iOS 12.0, *)) {
308 if ([hint isEqualToString:
@"oneTimeCode"]) {
309 return UITextContentTypeOneTimeCode;
312 if ([hint isEqualToString:
@"newPassword"]) {
313 return UITextContentTypeNewPassword;
382 typedef NS_ENUM(NSInteger, FlutterAutofillType) {
386 kFlutterAutofillTypeNone,
387 kFlutterAutofillTypeRegular,
388 kFlutterAutofillTypePassword,
398 if (isSecureTextEntry) {
405 if ([contentType isEqualToString:UITextContentTypePassword] ||
406 [contentType isEqualToString:UITextContentTypeUsername]) {
410 if (@available(iOS 12.0, *)) {
411 if ([contentType isEqualToString:UITextContentTypeNewPassword]) {
421 return kFlutterAutofillTypePassword;
426 return kFlutterAutofillTypePassword;
431 return !autofill || [contentType isEqualToString:
@""] ? kFlutterAutofillTypeNone
432 : kFlutterAutofillTypeRegular;
436 return fabsf(x - y) <= delta;
462 CGRect selectionRect,
463 BOOL selectionRectIsRTL,
464 BOOL useTrailingBoundaryOfSelectionRect,
465 CGRect otherSelectionRect,
466 BOOL otherSelectionRectIsRTL,
467 CGFloat verticalPrecision) {
469 if (CGRectContainsPoint(
471 selectionRect.origin.x + ((useTrailingBoundaryOfSelectionRect ^ selectionRectIsRTL)
472 ? 0.5 * selectionRect.size.width
474 selectionRect.origin.y, 0.5 * selectionRect.size.width, selectionRect.size.height),
479 CGPoint pointForSelectionRect = CGPointMake(
480 selectionRect.origin.x +
481 (selectionRectIsRTL ^ useTrailingBoundaryOfSelectionRect ? selectionRect.size.width : 0),
482 selectionRect.origin.y + selectionRect.size.height * 0.5);
483 float yDist = fabs(pointForSelectionRect.y - point.y);
484 float xDist = fabs(pointForSelectionRect.x - point.x);
487 CGPoint pointForOtherSelectionRect = CGPointMake(
488 otherSelectionRect.origin.x + (otherSelectionRectIsRTL ? otherSelectionRect.size.width : 0),
489 otherSelectionRect.origin.y + otherSelectionRect.size.height * 0.5);
490 float yDistOther = fabs(pointForOtherSelectionRect.y - point.y);
491 float xDistOther = fabs(pointForOtherSelectionRect.x - point.x);
496 BOOL isCloserVertically = yDist < yDistOther - verticalPrecision;
498 BOOL isAboveBottomOfLine = point.y <= selectionRect.origin.y + selectionRect.size.height;
499 BOOL isCloserHorizontally = xDist < xDistOther;
500 BOOL isBelowBottomOfLine = point.y > selectionRect.origin.y + selectionRect.size.height;
503 if (selectionRectIsRTL) {
504 isFarther = selectionRect.origin.x < otherSelectionRect.origin.x;
506 isFarther = selectionRect.origin.x +
507 (useTrailingBoundaryOfSelectionRect ? selectionRect.size.width : 0) >
508 otherSelectionRect.origin.x;
510 return (isCloserVertically ||
511 (isEqualVertically &&
512 ((isAboveBottomOfLine && isCloserHorizontally) || (isBelowBottomOfLine && isFarther))));
515 #pragma mark - FlutterTextPosition
519 + (instancetype)positionWithIndex:(NSUInteger)index {
520 return [[
FlutterTextPosition alloc] initWithIndex:index affinity:UITextStorageDirectionForward];
523 + (instancetype)positionWithIndex:(NSUInteger)index affinity:(UITextStorageDirection)affinity {
527 - (instancetype)initWithIndex:(NSUInteger)index affinity:(UITextStorageDirection)affinity {
538 #pragma mark - FlutterTextRange
542 + (instancetype)rangeWithNSRange:(NSRange)range {
546 - (instancetype)initWithNSRange:(NSRange)range {
554 - (UITextPosition*)start {
556 affinity:UITextStorageDirectionForward];
559 - (UITextPosition*)end {
561 affinity:UITextStorageDirectionBackward];
565 return self.range.length == 0;
568 - (
id)copyWithZone:(NSZone*)zone {
573 return NSEqualRanges(
self.
range, other.
range);
577 #pragma mark - FlutterTokenizer
587 - (instancetype)initWithTextInput:(UIResponder<UITextInput>*)textInput {
589 @"The FlutterTokenizer can only be used in a FlutterTextInputView");
590 self = [
super initWithTextInput:textInput];
597 - (UITextRange*)rangeEnclosingPosition:(UITextPosition*)position
598 withGranularity:(UITextGranularity)granularity
599 inDirection:(UITextDirection)direction {
601 switch (granularity) {
602 case UITextGranularityLine:
605 result = [
self lineEnclosingPosition:position];
607 case UITextGranularityCharacter:
608 case UITextGranularityWord:
609 case UITextGranularitySentence:
610 case UITextGranularityParagraph:
611 case UITextGranularityDocument:
613 result = [
super rangeEnclosingPosition:position
614 withGranularity:granularity
615 inDirection:direction];
621 - (UITextRange*)lineEnclosingPosition:(UITextPosition*)position {
623 NSString* textAfter = [_textInputView
624 textInRange:[_textInputView textRangeFromPosition:position
625 toPosition:[_textInputView endOfDocument]]];
626 NSArray<NSString*>* linesAfter = [textAfter componentsSeparatedByString:@"\n"];
627 NSInteger offSetToLineBreak = [linesAfter firstObject].length;
628 UITextPosition* lineBreakAfter = [_textInputView positionFromPosition:position
629 offset:offSetToLineBreak];
631 NSString* textBefore = [_textInputView
632 textInRange:[_textInputView textRangeFromPosition:[_textInputView beginningOfDocument]
633 toPosition:position]];
634 NSArray<NSString*>* linesBefore = [textBefore componentsSeparatedByString:@"\n"];
635 NSInteger offSetFromLineBreak = [linesBefore lastObject].length;
636 UITextPosition* lineBreakBefore = [_textInputView positionFromPosition:position
637 offset:-offSetFromLineBreak];
639 return [_textInputView textRangeFromPosition:lineBreakBefore toPosition:lineBreakAfter];
644 #pragma mark - FlutterTextSelectionRect
648 @synthesize rect = _rect;
654 + (instancetype)selectionRectWithRectAndInfo:(CGRect)rect
655 position:(NSUInteger)position
656 writingDirection:(NSWritingDirection)writingDirection
657 containsStart:(BOOL)containsStart
658 containsEnd:(BOOL)containsEnd
659 isVertical:(BOOL)isVertical {
662 writingDirection:writingDirection
663 containsStart:containsStart
664 containsEnd:containsEnd
665 isVertical:isVertical];
668 + (instancetype)selectionRectWithRect:(CGRect)rect position:(NSUInteger)position {
671 writingDirection:NSWritingDirectionNatural
677 + (instancetype)selectionRectWithRect:(CGRect)rect
678 position:(NSUInteger)position
679 writingDirection:(NSWritingDirection)writingDirection {
682 writingDirection:writingDirection
688 - (instancetype)initWithRectAndInfo:(CGRect)rect
689 position:(NSUInteger)position
690 writingDirection:(NSWritingDirection)writingDirection
691 containsStart:(BOOL)containsStart
692 containsEnd:(BOOL)containsEnd
693 isVertical:(BOOL)isVertical {
707 return _writingDirection == NSWritingDirectionRightToLeft;
712 #pragma mark - FlutterTextPlaceholder
716 - (NSArray<UITextSelectionRect*>*)rects {
732 @property(nonatomic, retain, readonly) UITextField*
textField;
746 - (BOOL)isKindOfClass:(Class)aClass {
747 return [
super isKindOfClass:aClass] || (aClass == [UITextField class]);
750 - (NSMethodSignature*)methodSignatureForSelector:(
SEL)aSelector {
751 NSMethodSignature* signature = [
super methodSignatureForSelector:aSelector];
753 signature = [
self.textField methodSignatureForSelector:aSelector];
758 - (void)forwardInvocation:(NSInvocation*)anInvocation {
759 [anInvocation invokeWithTarget:self.textField];
765 @property(nonatomic, readonly, weak) id<FlutterTextInputDelegate> textInputDelegate;
766 @property(nonatomic, readonly) UIView* hostView;
771 @property(nonatomic, copy) NSString* autofillId;
772 @property(nonatomic, readonly) CATransform3D editableTransform;
773 @property(nonatomic, assign) CGRect markedRect;
775 @property(nonatomic, assign) BOOL preventCursorDismissWhenResignFirstResponder;
776 @property(nonatomic) BOOL isVisibleToAutofill;
777 @property(nonatomic, assign) BOOL accessibilityEnabled;
778 @property(nonatomic, assign)
int textInputClient;
782 @property(nonatomic, copy) NSString* temporarilyDeletedComposedCharacter;
784 - (void)setEditableTransform:(NSArray*)matrix;
788 int _textInputClient;
802 UITextInteraction* _textInteraction
API_AVAILABLE(ios(13.0));
805 @synthesize tokenizer = _tokenizer;
808 self = [
super initWithFrame:CGRectZero];
811 _textInputClient = 0;
813 _preventCursorDismissWhenResignFirstResponder = NO;
816 _text = [[NSMutableString alloc] init];
817 _markedText = [[NSMutableString alloc] init];
822 _pendingDeltas = [[NSMutableArray alloc] init];
825 _editableTransform = CATransform3D();
828 _autocapitalizationType = UITextAutocapitalizationTypeSentences;
829 _autocorrectionType = UITextAutocorrectionTypeDefault;
830 _spellCheckingType = UITextSpellCheckingTypeDefault;
831 _enablesReturnKeyAutomatically = NO;
832 _keyboardAppearance = UIKeyboardAppearanceDefault;
833 _keyboardType = UIKeyboardTypeDefault;
834 _returnKeyType = UIReturnKeyDone;
835 _secureTextEntry = NO;
836 _enableDeltaModel = NO;
838 _accessibilityEnabled = NO;
839 _smartQuotesType = UITextSmartQuotesTypeYes;
840 _smartDashesType = UITextSmartDashesTypeYes;
841 _selectionRects = [[NSArray alloc] init];
843 if (@available(iOS 14.0, *)) {
844 UIScribbleInteraction* interaction = [[UIScribbleInteraction alloc] initWithDelegate:self];
845 [
self addInteraction:interaction];
852 - (void)configureWithDictionary:(NSDictionary*)configuration {
853 NSDictionary* inputType = configuration[kKeyboardType];
855 NSDictionary* autofill = configuration[kAutofillProperties];
857 self.secureTextEntry = [configuration[kSecureTextEntry] boolValue];
858 self.enableDeltaModel = [configuration[kEnableDeltaModel] boolValue];
865 NSString* smartDashesType = configuration[kSmartDashesType];
867 bool smartDashesIsDisabled = smartDashesType && [smartDashesType isEqualToString:@"0"];
868 self.smartDashesType = smartDashesIsDisabled ? UITextSmartDashesTypeNo : UITextSmartDashesTypeYes;
869 NSString* smartQuotesType = configuration[kSmartQuotesType];
871 bool smartQuotesIsDisabled = smartQuotesType && [smartQuotesType isEqualToString:@"0"];
872 self.smartQuotesType = smartQuotesIsDisabled ? UITextSmartQuotesTypeNo : UITextSmartQuotesTypeYes;
874 self.keyboardAppearance = UIKeyboardAppearanceDark;
876 self.keyboardAppearance = UIKeyboardAppearanceLight;
878 self.keyboardAppearance = UIKeyboardAppearanceDefault;
880 NSString* autocorrect = configuration[kAutocorrectionType];
881 bool autocorrectIsDisabled = autocorrect && ![autocorrect boolValue];
882 self.autocorrectionType =
883 autocorrectIsDisabled ? UITextAutocorrectionTypeNo : UITextAutocorrectionTypeDefault;
884 self.spellCheckingType =
885 autocorrectIsDisabled ? UITextSpellCheckingTypeNo : UITextSpellCheckingTypeDefault;
887 if (autofill == nil) {
888 self.textContentType =
@"";
891 [
self setTextInputState:autofill[kAutofillEditingValue]];
892 NSAssert(_autofillId,
@"The autofill configuration must contain an autofill id");
896 self.isVisibleToAutofill = autofill || _secureTextEntry;
899 - (UITextContentType)textContentType {
900 return _textContentType;
906 - (UIColor*)insertionPointColor {
907 return [UIColor clearColor];
910 - (UIColor*)selectionBarColor {
911 return [UIColor clearColor];
914 - (UIColor*)selectionHighlightColor {
915 return [UIColor clearColor];
918 - (UIInputViewController*)inputViewController {
933 - (void)setTextInputClient:(
int)client {
934 _textInputClient = client;
938 - (UITextInteraction*)textInteraction
API_AVAILABLE(ios(13.0)) {
939 if (!_textInteraction) {
940 _textInteraction = [UITextInteraction textInteractionForMode:UITextInteractionModeEditable];
941 _textInteraction.textInput =
self;
943 return _textInteraction;
946 - (void)setTextInputState:(NSDictionary*)state {
947 if (@available(iOS 13.0, *)) {
954 [
self addInteraction:self.textInteraction];
958 NSString* newText = state[@"text"];
959 BOOL textChanged = ![
self.text isEqualToString:newText];
961 [
self.inputDelegate textWillChange:self];
962 [
self.text setString:newText];
964 NSInteger composingBase = [state[@"composingBase"] intValue];
965 NSInteger composingExtent = [state[@"composingExtent"] intValue];
966 NSRange composingRange = [
self clampSelection:NSMakeRange(MIN(composingBase, composingExtent),
967 ABS(composingBase - composingExtent))
970 self.markedTextRange =
973 NSRange selectedRange = [
self clampSelectionFromBase:[state[@"selectionBase"] intValue]
974 extent:[state[@"selectionExtent"] intValue]
977 NSRange oldSelectedRange = [(
FlutterTextRange*)
self.selectedTextRange range];
978 if (!NSEqualRanges(selectedRange, oldSelectedRange)) {
979 [
self.inputDelegate selectionWillChange:self];
987 [
self.inputDelegate selectionDidChange:self];
991 [
self.inputDelegate textDidChange:self];
994 if (@available(iOS 13.0, *)) {
995 if (_textInteraction) {
996 [
self removeInteraction:_textInteraction];
1002 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
1003 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1004 [
self resetScribbleInteractionStatusIfEnding];
1005 [
self.viewResponder touchesBegan:touches withEvent:event];
1008 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
1009 [
self.viewResponder touchesMoved:touches withEvent:event];
1012 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
1013 [
self.viewResponder touchesEnded:touches withEvent:event];
1016 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1017 [
self.viewResponder touchesCancelled:touches withEvent:event];
1020 - (void)touchesEstimatedPropertiesUpdated:(NSSet*)touches {
1021 [
self.viewResponder touchesEstimatedPropertiesUpdated:touches];
1031 - (NSRange)clampSelectionFromBase:(
int)selectionBase
1032 extent:(
int)selectionExtent
1033 forText:(NSString*)text {
1034 int loc = MIN(selectionBase, selectionExtent);
1035 int len = ABS(selectionExtent - selectionBase);
1036 return loc < 0 ? NSMakeRange(0, 0)
1037 : [
self clampSelection:NSMakeRange(loc, len) forText:
self.text];
1040 - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text {
1041 NSUInteger start = MIN(MAX(range.location, 0), text.length);
1042 NSUInteger length = MIN(range.length, text.length - start);
1043 return NSMakeRange(start, length);
1046 - (BOOL)isVisibleToAutofill {
1047 return self.frame.size.width > 0 &&
self.frame.size.height > 0;
1055 - (void)setIsVisibleToAutofill:(BOOL)isVisibleToAutofill {
1058 self.frame = isVisibleToAutofill ? CGRectMake(0, 0, 1, 1) : CGRectZero;
1061 #pragma mark UIScribbleInteractionDelegate
1066 if (@available(iOS 14.0, *)) {
1067 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
1074 - (void)scribbleInteractionWillBeginWriting:(UIScribbleInteraction*)interaction
1075 API_AVAILABLE(ios(14.0)) {
1077 [
self.textInputDelegate flutterTextInputViewScribbleInteractionBegan:self];
1080 - (void)scribbleInteractionDidFinishWriting:(UIScribbleInteraction*)interaction
1081 API_AVAILABLE(ios(14.0)) {
1083 [
self.textInputDelegate flutterTextInputViewScribbleInteractionFinished:self];
1086 - (BOOL)scribbleInteraction:(UIScribbleInteraction*)interaction
1087 shouldBeginAtLocation:(CGPoint)location API_AVAILABLE(ios(14.0)) {
1091 - (BOOL)scribbleInteractionShouldDelayFocus:(UIScribbleInteraction*)interaction
1092 API_AVAILABLE(ios(14.0)) {
1096 #pragma mark - UIResponder Overrides
1098 - (BOOL)canBecomeFirstResponder {
1103 return _textInputClient != 0;
1106 - (BOOL)resignFirstResponder {
1107 BOOL success = [
super resignFirstResponder];
1109 if (!_preventCursorDismissWhenResignFirstResponder) {
1110 [
self.textInputDelegate flutterTextInputView:self
1111 didResignFirstResponderWithTextInputClient:_textInputClient];
1117 - (BOOL)canPerformAction:(
SEL)action withSender:(
id)sender {
1123 if (
action ==
@selector(paste:)) {
1125 return [UIPasteboard generalPasteboard].string != nil;
1128 return [
super canPerformAction:action withSender:sender];
1131 #pragma mark - UIResponderStandardEditActions Overrides
1133 - (void)cut:(
id)sender {
1134 [UIPasteboard generalPasteboard].string = [
self textInRange:_selectedTextRange];
1135 [
self replaceRange:_selectedTextRange withText:@""];
1138 - (void)copy:(
id)sender {
1139 [UIPasteboard generalPasteboard].string = [
self textInRange:_selectedTextRange];
1142 - (void)paste:(
id)sender {
1143 NSString* pasteboardString = [UIPasteboard generalPasteboard].string;
1144 if (pasteboardString != nil) {
1145 [
self insertText:pasteboardString];
1149 - (void)delete:(
id)sender {
1150 [
self replaceRange:_selectedTextRange withText:@""];
1153 - (void)selectAll:(
id)sender {
1154 [
self setSelectedTextRange:[
self textRangeFromPosition:[
self beginningOfDocument]
1155 toPosition:[
self endOfDocument]]];
1158 #pragma mark - UITextInput Overrides
1160 - (
id<UITextInputTokenizer>)tokenizer {
1161 if (_tokenizer == nil) {
1168 return [_selectedTextRange copy];
1172 - (void)setSelectedTextRangeLocal:(UITextRange*)selectedTextRange {
1177 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, flutterTextRange.range)] copy];
1184 - (void)setSelectedTextRange:(UITextRange*)selectedTextRange {
1189 [
self setSelectedTextRangeLocal:selectedTextRange];
1191 if (_enableDeltaModel) {
1192 [
self updateEditingStateWithDelta:flutter::TextEditingDelta([
self.text UTF8String])];
1194 [
self updateEditingState];
1198 _scribbleFocusStatus == FlutterScribbleFocusStatusFocused) {
1202 if (flutterTextRange.
range.length > 0) {
1203 [
self.textInputDelegate flutterTextInputView:self showToolbar:_textInputClient];
1207 [
self resetScribbleInteractionStatusIfEnding];
1210 - (
id)insertDictationResultPlaceholder {
1214 - (void)removeDictationResultPlaceholder:(
id)placeholder willInsertResult:(BOOL)willInsertResult {
1217 - (NSString*)textInRange:(UITextRange*)range {
1222 @"Expected a FlutterTextRange for range (got %@).", [range
class]);
1224 NSAssert(textRange.location != NSNotFound,
@"Expected a valid text range.");
1226 NSUInteger location = MIN(textRange.location,
self.text.length);
1227 NSUInteger length = MIN(
self.text.length - location, textRange.length);
1228 NSRange safeRange = NSMakeRange(location, length);
1229 return [
self.text substringWithRange:safeRange];
1234 - (void)replaceRangeLocal:(NSRange)range withText:(NSString*)text {
1240 NSRange intersectionRange = NSIntersectionRange(range, selectedRange);
1241 if (range.location <= selectedRange.location) {
1242 selectedRange.location += text.length - range.length;
1244 if (intersectionRange.location != NSNotFound) {
1245 selectedRange.location += intersectionRange.length;
1246 selectedRange.length -= intersectionRange.length;
1249 [
self.text replaceCharactersInRange:[
self clampSelection:range forText:self.text]
1253 forText:self.text]]];
1256 - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
1257 NSString* textBeforeChange = [
self.text copy];
1259 [
self replaceRangeLocal:replaceRange withText:text];
1260 if (_enableDeltaModel) {
1261 NSRange nextReplaceRange = [
self clampSelection:replaceRange forText:textBeforeChange];
1262 [
self updateEditingStateWithDelta:flutter::TextEditingDelta(
1263 [textBeforeChange UTF8String],
1265 nextReplaceRange.location,
1266 nextReplaceRange.location + nextReplaceRange.length),
1267 [text UTF8String])];
1269 [
self updateEditingState];
1273 - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
1276 self.temporarilyDeletedComposedCharacter = nil;
1278 if (
self.
returnKeyType == UIReturnKeyDefault && [text isEqualToString:
@"\n"]) {
1279 [
self.textInputDelegate flutterTextInputView:self
1280 performAction:FlutterTextInputActionNewline
1281 withClient:_textInputClient];
1285 if ([text isEqualToString:
@"\n"]) {
1286 FlutterTextInputAction
action;
1288 case UIReturnKeyDefault:
1289 action = FlutterTextInputActionUnspecified;
1291 case UIReturnKeyDone:
1292 action = FlutterTextInputActionDone;
1295 action = FlutterTextInputActionGo;
1297 case UIReturnKeySend:
1298 action = FlutterTextInputActionSend;
1300 case UIReturnKeySearch:
1301 case UIReturnKeyGoogle:
1302 case UIReturnKeyYahoo:
1303 action = FlutterTextInputActionSearch;
1305 case UIReturnKeyNext:
1306 action = FlutterTextInputActionNext;
1308 case UIReturnKeyContinue:
1309 action = FlutterTextInputActionContinue;
1311 case UIReturnKeyJoin:
1312 action = FlutterTextInputActionJoin;
1314 case UIReturnKeyRoute:
1315 action = FlutterTextInputActionRoute;
1317 case UIReturnKeyEmergencyCall:
1318 action = FlutterTextInputActionEmergencyCall;
1322 [
self.textInputDelegate flutterTextInputView:self
1323 performAction:action
1324 withClient:_textInputClient];
1331 - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelectedRange {
1332 NSString* textBeforeChange = [
self.text copy];
1335 NSRange actualReplacedRange;
1338 _scribbleFocusStatus != FlutterScribbleFocusStatusUnfocused) {
1348 [
self replaceRangeLocal:markedTextRange withText:markedText];
1353 actualReplacedRange = selectedRange;
1354 [
self replaceRangeLocal:selectedRange withText:markedText];
1358 self.markedTextRange =
1361 NSUInteger selectionLocation = markedSelectedRange.location +
markedTextRange.location;
1362 selectedRange = NSMakeRange(selectionLocation, markedSelectedRange.length);
1365 forText:self.text]]];
1366 if (_enableDeltaModel) {
1367 NSRange nextReplaceRange = [
self clampSelection:actualReplacedRange forText:textBeforeChange];
1368 [
self updateEditingStateWithDelta:flutter::TextEditingDelta(
1369 [textBeforeChange UTF8String],
1371 nextReplaceRange.location,
1372 nextReplaceRange.location + nextReplaceRange.length),
1373 [markedText UTF8String])];
1375 [
self updateEditingState];
1379 - (void)unmarkText {
1383 self.markedTextRange = nil;
1384 if (_enableDeltaModel) {
1385 [
self updateEditingStateWithDelta:flutter::TextEditingDelta([
self.text UTF8String])];
1387 [
self updateEditingState];
1391 - (UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition
1392 toPosition:(UITextPosition*)toPosition {
1395 if (toIndex >= fromIndex) {
1408 - (NSUInteger)decrementOffsetPosition:(NSUInteger)position {
1409 return fml::RangeForCharacterAtIndex(
self.text, MAX(0, position - 1)).location;
1412 - (NSUInteger)incrementOffsetPosition:(NSUInteger)position {
1413 NSRange charRange = fml::RangeForCharacterAtIndex(
self.text, position);
1414 return MIN(position + charRange.length,
self.text.length);
1417 - (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInteger)offset {
1420 NSInteger newLocation = (NSInteger)offsetPosition + offset;
1421 if (newLocation < 0 || newLocation > (NSInteger)
self.text.length) {
1430 for (NSInteger i = 0; i < offset && offsetPosition <
self.text.length; ++i) {
1431 offsetPosition = [
self incrementOffsetPosition:offsetPosition];
1434 for (NSInteger i = 0; i < ABS(offset) && offsetPosition > 0; ++i) {
1435 offsetPosition = [
self decrementOffsetPosition:offsetPosition];
1441 - (UITextPosition*)positionFromPosition:(UITextPosition*)position
1442 inDirection:(UITextLayoutDirection)direction
1443 offset:(NSInteger)offset {
1445 switch (direction) {
1446 case UITextLayoutDirectionLeft:
1447 case UITextLayoutDirectionUp:
1448 return [
self positionFromPosition:position offset:offset * -1];
1449 case UITextLayoutDirectionRight:
1450 case UITextLayoutDirectionDown:
1451 return [
self positionFromPosition:position offset:1];
1455 - (UITextPosition*)beginningOfDocument {
1459 - (UITextPosition*)endOfDocument {
1461 affinity:UITextStorageDirectionBackward];
1464 - (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other {
1467 if (positionIndex < otherIndex) {
1468 return NSOrderedAscending;
1470 if (positionIndex > otherIndex) {
1471 return NSOrderedDescending;
1475 if (positionAffinity == otherAffinity) {
1476 return NSOrderedSame;
1478 if (positionAffinity == UITextStorageDirectionBackward) {
1480 return NSOrderedAscending;
1483 return NSOrderedDescending;
1486 - (NSInteger)offsetFromPosition:(UITextPosition*)from toPosition:(UITextPosition*)toPosition {
1490 - (UITextPosition*)positionWithinRange:(UITextRange*)range
1491 farthestInDirection:(UITextLayoutDirection)direction {
1493 UITextStorageDirection affinity;
1494 switch (direction) {
1495 case UITextLayoutDirectionLeft:
1496 case UITextLayoutDirectionUp:
1498 affinity = UITextStorageDirectionForward;
1500 case UITextLayoutDirectionRight:
1501 case UITextLayoutDirectionDown:
1503 affinity = UITextStorageDirectionBackward;
1509 - (UITextRange*)characterRangeByExtendingPosition:(UITextPosition*)position
1510 inDirection:(UITextLayoutDirection)direction {
1512 NSUInteger startIndex;
1513 NSUInteger endIndex;
1514 switch (direction) {
1515 case UITextLayoutDirectionLeft:
1516 case UITextLayoutDirectionUp:
1517 startIndex = [
self decrementOffsetPosition:positionIndex];
1518 endIndex = positionIndex;
1520 case UITextLayoutDirectionRight:
1521 case UITextLayoutDirectionDown:
1522 startIndex = positionIndex;
1523 endIndex = [
self incrementOffsetPosition:positionIndex];
1529 #pragma mark - UITextInput text direction handling
1531 - (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position
1532 inDirection:(UITextStorageDirection)direction {
1534 return UITextWritingDirectionNatural;
1537 - (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection
1538 forRange:(UITextRange*)range {
1542 #pragma mark - UITextInput cursor, selection rect handling
1544 - (void)setMarkedRect:(CGRect)markedRect {
1545 _markedRect = markedRect;
1552 - (void)setEditableTransform:(NSArray*)matrix {
1553 CATransform3D* transform = &_editableTransform;
1555 transform->m11 = [matrix[0] doubleValue];
1556 transform->m12 = [matrix[1] doubleValue];
1557 transform->m13 = [matrix[2] doubleValue];
1558 transform->m14 = [matrix[3] doubleValue];
1560 transform->m21 = [matrix[4] doubleValue];
1561 transform->m22 = [matrix[5] doubleValue];
1562 transform->m23 = [matrix[6] doubleValue];
1563 transform->m24 = [matrix[7] doubleValue];
1565 transform->m31 = [matrix[8] doubleValue];
1566 transform->m32 = [matrix[9] doubleValue];
1567 transform->m33 = [matrix[10] doubleValue];
1568 transform->m34 = [matrix[11] doubleValue];
1570 transform->m41 = [matrix[12] doubleValue];
1571 transform->m42 = [matrix[13] doubleValue];
1572 transform->m43 = [matrix[14] doubleValue];
1573 transform->m44 = [matrix[15] doubleValue];
1581 - (CGRect)localRectFromFrameworkTransform:(CGRect)incomingRect {
1582 CGPoint points[] = {
1583 incomingRect.origin,
1584 CGPointMake(incomingRect.origin.x, incomingRect.origin.y + incomingRect.size.height),
1585 CGPointMake(incomingRect.origin.x + incomingRect.size.width, incomingRect.origin.y),
1586 CGPointMake(incomingRect.origin.x + incomingRect.size.width,
1587 incomingRect.origin.y + incomingRect.size.height)};
1589 CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
1590 CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
1592 for (
int i = 0; i < 4; i++) {
1593 const CGPoint point = points[i];
1595 CGFloat x = _editableTransform.m11 * point.x + _editableTransform.m21 * point.y +
1596 _editableTransform.m41;
1597 CGFloat y = _editableTransform.m12 * point.x + _editableTransform.m22 * point.y +
1598 _editableTransform.m42;
1600 const CGFloat w = _editableTransform.m14 * point.x + _editableTransform.m24 * point.y +
1601 _editableTransform.m44;
1605 }
else if (w != 1.0) {
1610 origin.x = MIN(origin.x, x);
1611 origin.y = MIN(origin.y, y);
1612 farthest.x = MAX(farthest.x, x);
1613 farthest.y = MAX(farthest.y, y);
1615 return CGRectMake(origin.x, origin.y, farthest.x - origin.x, farthest.y - origin.y);
1624 - (CGRect)firstRectForRange:(UITextRange*)range {
1626 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
1628 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
1631 if (_markedTextRange != nil) {
1642 CGRect rect = _markedRect;
1643 if (CGRectIsEmpty(rect)) {
1644 rect = CGRectInset(rect, -0.1, 0);
1650 NSAssert(hostView == nil || [
self isDescendantOfView:hostView],
@"%@ is not a descendant of %@",
1652 return hostView ? [hostView convertRect:_cachedFirstRect toView:self] :
_cachedFirstRect;
1656 _scribbleFocusStatus == FlutterScribbleFocusStatusUnfocused) {
1657 if (@available(iOS 17.0, *)) {
1667 [
self.textInputDelegate flutterTextInputView:self
1668 showAutocorrectionPromptRectForStart:start
1670 withClient:_textInputClient];
1674 NSUInteger first = start;
1679 CGRect startSelectionRect = CGRectNull;
1680 CGRect endSelectionRect = CGRectNull;
1683 CGFloat minY = CGFLOAT_MAX;
1684 CGFloat maxY = CGFLOAT_MIN;
1687 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1688 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1689 BOOL startsOnOrBeforeStartOfRange = _selectionRects[i].position <= first;
1690 BOOL isLastSelectionRect = i + 1 == [_selectionRects count];
1691 BOOL endOfTextIsAfterStartOfRange = isLastSelectionRect && textRange.
range.length > first;
1692 BOOL nextSelectionRectIsAfterStartOfRange =
1693 !isLastSelectionRect && _selectionRects[i + 1].position > first;
1694 if (startsOnOrBeforeStartOfRange &&
1695 (endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
1697 if (@available(iOS 17, *)) {
1698 startSelectionRect = _selectionRects[i].rect;
1700 return _selectionRects[i].rect;
1703 if (!CGRectIsNull(startSelectionRect)) {
1704 minY = fmin(minY, CGRectGetMinY(_selectionRects[i].rect));
1705 maxY = fmax(maxY, CGRectGetMaxY(_selectionRects[i].rect));
1706 BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1;
1707 BOOL nextSelectionRectIsOnNextLine =
1708 !isLastSelectionRect &&
1713 CGRectGetMidY(_selectionRects[i + 1].rect) > CGRectGetMaxY(_selectionRects[i].rect);
1714 if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectionRectIsOnNextLine) {
1715 endSelectionRect = _selectionRects[i].rect;
1720 if (CGRectIsNull(startSelectionRect) || CGRectIsNull(endSelectionRect)) {
1724 CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
1725 CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
1726 return CGRectMake(minX, minY, maxX - minX, maxY - minY);
1730 - (CGRect)caretRectForPosition:(UITextPosition*)position {
1734 NSArray<UITextSelectionRect*>* rects = [
self
1736 rangeWithNSRange:fml::RangeForCharactersInRange(
1740 (index >= (NSInteger)self.text.length)
1743 if (rects.count == 0) {
1749 CGRect characterAfterCaret = rects[0].rect;
1754 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1755 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1757 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1758 characterAfterCaret.size.height);
1760 }
else if (rects.count == 2 && affinity == UITextStorageDirectionForward) {
1763 CGRect characterAfterCaret = rects[1].rect;
1768 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1769 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1771 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1772 characterAfterCaret.size.height);
1781 CGRect characterBeforeCaret = rects[0].rect;
1784 return CGRectMake(characterBeforeCaret.origin.x, characterBeforeCaret.origin.y, 0,
1785 characterBeforeCaret.size.height);
1787 return CGRectMake(characterBeforeCaret.origin.x + characterBeforeCaret.size.width,
1788 characterBeforeCaret.origin.y, 0, characterBeforeCaret.size.height);
1792 - (UITextPosition*)closestPositionToPoint:(CGPoint)point {
1793 if ([_selectionRects count] == 0) {
1795 @"Expected a FlutterTextPosition for position (got %@).",
1798 UITextStorageDirection currentAffinity =
1804 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1805 return [
self closestPositionToPoint:point withinRange:range];
1808 - (NSArray*)selectionRectsForRange:(UITextRange*)range {
1816 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
1818 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
1821 NSMutableArray* rects = [[NSMutableArray alloc] init];
1822 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1823 if (_selectionRects[i].position >= start &&
1824 (_selectionRects[i].position < end ||
1825 (start == end && _selectionRects[i].position <= end))) {
1826 float width = _selectionRects[i].rect.size.width;
1830 CGRect rect = CGRectMake(_selectionRects[i].rect.origin.x, _selectionRects[i].rect.origin.y,
1831 width, _selectionRects[i].rect.size.height);
1834 position:_selectionRects[i].position
1838 self.text, NSMakeRange(0, self.text.length))
1841 [rects addObject:selectionRect];
1847 - (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range {
1849 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
1851 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
1861 NSUInteger _closestRectIndex = 0;
1862 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1863 NSUInteger position = _selectionRects[i].position;
1864 if (position >= start && position <= end) {
1867 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
1868 NO, _selectionRects[_closestRectIndex].rect,
1869 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
1871 _closestRectIndex = i;
1878 affinity:UITextStorageDirectionForward];
1884 for (NSUInteger i = MAX(0, _closestRectIndex - 1);
1885 i < MIN(_closestRectIndex + 2, [_selectionRects count]); i++) {
1886 NSUInteger position = _selectionRects[i].position + 1;
1887 if (position >= start && position <= end) {
1889 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
1890 YES, _selectionRects[_closestRectIndex].rect,
1891 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
1894 affinity:UITextStorageDirectionBackward];
1899 return closestPosition;
1902 - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
1905 return [
FlutterTextRange rangeWithNSRange:fml::RangeForCharacterAtIndex(self.text, currentIndex)];
1936 - (void)beginFloatingCursorAtPoint:(CGPoint)point {
1953 [
self.textInputDelegate flutterTextInputView:self
1954 updateFloatingCursor:FlutterFloatingCursorDragStateStart
1955 withClient:_textInputClient
1956 withPosition:@{@"X" : @0, @"Y" : @0}];
1959 - (void)updateFloatingCursorAtPoint:(CGPoint)point {
1960 [
self.textInputDelegate flutterTextInputView:self
1961 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
1962 withClient:_textInputClient
1964 @"X" : @(point.x - _floatingCursorOffset.x),
1965 @"Y" : @(point.y - _floatingCursorOffset.y)
1969 - (void)endFloatingCursor {
1971 [
self.textInputDelegate flutterTextInputView:self
1972 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
1973 withClient:_textInputClient
1974 withPosition:@{@"X" : @0, @"Y" : @0}];
1977 #pragma mark - UIKeyInput Overrides
1979 - (void)updateEditingState {
1984 NSInteger composingBase = -1;
1985 NSInteger composingExtent = -1;
1990 NSDictionary* state = @{
1991 @"selectionBase" : @(selectionBase),
1992 @"selectionExtent" : @(selectionExtent),
1994 @"selectionIsDirectional" : @(
false),
1995 @"composingBase" : @(composingBase),
1996 @"composingExtent" : @(composingExtent),
1997 @"text" : [NSString stringWithString:self.text],
2000 if (_textInputClient == 0 && _autofillId != nil) {
2001 [
self.textInputDelegate flutterTextInputView:self
2002 updateEditingClient:_textInputClient
2004 withTag:_autofillId];
2006 [
self.textInputDelegate flutterTextInputView:self
2007 updateEditingClient:_textInputClient
2012 - (void)updateEditingStateWithDelta:(
flutter::TextEditingDelta)delta {
2017 NSInteger composingBase = -1;
2018 NSInteger composingExtent = -1;
2024 NSDictionary* deltaToFramework = @{
2025 @"oldText" : @(delta.old_text().c_str()),
2026 @"deltaText" : @(delta.delta_text().c_str()),
2027 @"deltaStart" : @(delta.delta_start()),
2028 @"deltaEnd" : @(delta.delta_end()),
2029 @"selectionBase" : @(selectionBase),
2030 @"selectionExtent" : @(selectionExtent),
2032 @"selectionIsDirectional" : @(
false),
2033 @"composingBase" : @(composingBase),
2034 @"composingExtent" : @(composingExtent),
2037 [_pendingDeltas addObject:deltaToFramework];
2039 if (_pendingDeltas.count == 1) {
2041 dispatch_async(dispatch_get_main_queue(), ^{
2043 if (strongSelf && strongSelf.pendingDeltas.count > 0) {
2044 NSDictionary* deltas = @{
2045 @"deltas" : strongSelf.pendingDeltas,
2048 [strongSelf.textInputDelegate flutterTextInputView:strongSelf
2049 updateEditingClient:strongSelf->_textInputClient
2051 [strongSelf.pendingDeltas removeAllObjects];
2058 return self.text.length > 0;
2061 - (void)insertText:(NSString*)text {
2062 if (
self.temporarilyDeletedComposedCharacter.length > 0 && text.length == 1 && !text.UTF8String &&
2063 [text characterAtIndex:0] == [
self.temporarilyDeletedComposedCharacter characterAtIndex:0]) {
2067 text =
self.temporarilyDeletedComposedCharacter;
2068 self.temporarilyDeletedComposedCharacter = nil;
2071 NSMutableArray<FlutterTextSelectionRect*>* copiedRects =
2072 [[NSMutableArray alloc] initWithCapacity:[_selectionRects count]];
2074 @"Expected a FlutterTextPosition for position (got %@).",
2077 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2078 NSUInteger rectPosition = _selectionRects[i].position;
2079 if (rectPosition == insertPosition) {
2080 for (NSUInteger j = 0; j <= text.length; j++) {
2087 if (rectPosition > insertPosition) {
2088 rectPosition = rectPosition + text.length;
2097 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2098 [
self resetScribbleInteractionStatusIfEnding];
2099 self.selectionRects = copiedRects;
2101 [
self replaceRange:_selectedTextRange withText:text];
2104 - (UITextPlaceholder*)insertTextPlaceholderWithSize:(CGSize)size API_AVAILABLE(ios(13.0)) {
2105 [
self.textInputDelegate flutterTextInputView:self
2106 insertTextPlaceholderWithSize:size
2107 withClient:_textInputClient];
2112 - (void)removeTextPlaceholder:(UITextPlaceholder*)textPlaceholder API_AVAILABLE(ios(13.0)) {
2114 [
self.textInputDelegate flutterTextInputView:self removeTextPlaceholder:_textInputClient];
2117 - (void)deleteBackward {
2119 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2120 [
self resetScribbleInteractionStatusIfEnding];
2137 if (oldRange.location > 0) {
2138 NSRange newRange = NSMakeRange(oldRange.location - 1, 1);
2142 NSRange charRange = fml::RangeForCharacterAtIndex(
self.text, oldRange.location - 1);
2143 if (
IsEmoji(
self.text, charRange)) {
2144 newRange = NSMakeRange(charRange.location, oldRange.location - charRange.location);
2156 NSString* deletedText = [
self.text substringWithRange:_selectedTextRange.range];
2157 NSRange deleteFirstCharacterRange = fml::RangeForCharacterAtIndex(deletedText, 0);
2158 self.temporarilyDeletedComposedCharacter =
2159 [deletedText substringWithRange:deleteFirstCharacterRange];
2161 [
self replaceRange:_selectedTextRange withText:@""];
2165 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(
id)target {
2166 UIAccessibilityPostNotification(notification, target);
2169 - (void)accessibilityElementDidBecomeFocused {
2170 if ([
self accessibilityElementIsFocused]) {
2174 FML_DCHECK(_backingTextInputAccessibilityObject);
2175 [
self postAccessibilityNotification:UIAccessibilityScreenChangedNotification
2176 target:_backingTextInputAccessibilityObject];
2180 - (BOOL)accessibilityElementsHidden {
2181 return !_accessibilityEnabled;
2190 #pragma mark - Key Events Handling
2191 - (void)pressesBegan:(NSSet<UIPress*>*)presses
2192 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2193 [_textInputPlugin.viewController pressesBegan:presses withEvent:event];
2196 - (void)pressesChanged:(NSSet<UIPress*>*)presses
2197 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2198 [_textInputPlugin.viewController pressesChanged:presses withEvent:event];
2201 - (void)pressesEnded:(NSSet<UIPress*>*)presses
2202 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2203 [_textInputPlugin.viewController pressesEnded:presses withEvent:event];
2206 - (void)pressesCancelled:(NSSet<UIPress*>*)presses
2207 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2208 [_textInputPlugin.viewController pressesCancelled:presses withEvent:event];
2237 - (BOOL)accessibilityElementsHidden {
2244 - (void)enableActiveViewAccessibility;
2261 - (void)enableActiveViewAccessibility {
2262 [
self.target enableActiveViewAccessibility];
2269 @property(nonatomic, readonly)
2270 NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
2275 @property(nonatomic, strong) UIView* keyboardViewContainer;
2276 @property(nonatomic, strong) UIView* keyboardView;
2277 @property(nonatomic, strong) UIView* cachedFirstResponder;
2278 @property(nonatomic, assign) CGRect keyboardRect;
2279 @property(nonatomic, assign) CGFloat previousPointerYPosition;
2280 @property(nonatomic, assign) CGFloat pointerYVelocity;
2284 NSTimer* _enableFlutterTextInputViewAccessibilityTimer;
2288 self = [
super init];
2291 _textInputDelegate = textInputDelegate;
2292 _autofillContext = [[NSMutableDictionary alloc] init];
2294 _scribbleElements = [[NSMutableDictionary alloc] init];
2295 _keyboardViewContainer = [[UIView alloc] init];
2297 [[NSNotificationCenter defaultCenter] addObserver:self
2298 selector:@selector(handleKeyboardWillShow:)
2299 name:UIKeyboardWillShowNotification
2306 - (void)handleKeyboardWillShow:(NSNotification*)notification {
2307 NSDictionary* keyboardInfo = [notification userInfo];
2308 NSValue* keyboardFrameEnd = [keyboardInfo valueForKey:UIKeyboardFrameEndUserInfoKey];
2309 _keyboardRect = [keyboardFrameEnd CGRectValue];
2313 [
self hideTextInput];
2316 - (void)removeEnableFlutterTextInputViewAccessibilityTimer {
2317 if (_enableFlutterTextInputViewAccessibilityTimer) {
2318 [_enableFlutterTextInputViewAccessibilityTimer invalidate];
2319 _enableFlutterTextInputViewAccessibilityTimer = nil;
2323 - (UIView<UITextInput>*)textInputView {
2328 NSString* method = call.
method;
2331 [
self showTextInput];
2333 }
else if ([method isEqualToString:
kHideMethod]) {
2334 [
self hideTextInput];
2337 [
self setTextInputClient:[args[0] intValue] withConfiguration:args[1]];
2341 [
self setPlatformViewTextInputClient];
2344 [
self setTextInputEditingState:args];
2347 [
self clearTextInputClient];
2350 [
self setEditableSizeAndTransform:args];
2353 [
self updateMarkedRect:args];
2356 [
self triggerAutofillSave:[args boolValue]];
2362 [
self setSelectionRects:args];
2365 [
self setSelectionRects:args];
2368 [
self startLiveTextInput];
2371 [
self updateConfig:args];
2374 CGFloat pointerY = (CGFloat)[args[
@"pointerY"] doubleValue];
2375 [
self handlePointerMove:pointerY];
2378 CGFloat pointerY = (CGFloat)[args[
@"pointerY"] doubleValue];
2379 [
self handlePointerUp:pointerY];
2386 - (void)handlePointerUp:(CGFloat)pointerY {
2387 if (_keyboardView.superview != nil) {
2391 CGFloat screenHeight = screen.bounds.size.height;
2392 CGFloat keyboardHeight = _keyboardRect.size.height;
2394 BOOL shouldDismissKeyboardBasedOnVelocity = _pointerYVelocity < 0;
2395 [UIView animateWithDuration:kKeyboardAnimationTimeToCompleteion
2397 double keyboardDestination =
2398 shouldDismissKeyboardBasedOnVelocity ? screenHeight : screenHeight - keyboardHeight;
2399 _keyboardViewContainer.frame = CGRectMake(
2400 0, keyboardDestination, _viewController.flutterScreenIfViewLoaded.bounds.size.width,
2401 _keyboardViewContainer.frame.size.height);
2403 completion:^(BOOL finished) {
2404 if (shouldDismissKeyboardBasedOnVelocity) {
2405 [
self.textInputDelegate flutterTextInputView:self.activeView
2406 didResignFirstResponderWithTextInputClient:self.activeView.textInputClient];
2407 [
self dismissKeyboardScreenshot];
2409 [
self showKeyboardAndRemoveScreenshot];
2415 - (void)dismissKeyboardScreenshot {
2416 for (UIView* subView in _keyboardViewContainer.subviews) {
2417 [subView removeFromSuperview];
2421 - (void)showKeyboardAndRemoveScreenshot {
2422 [UIView setAnimationsEnabled:NO];
2423 [_cachedFirstResponder becomeFirstResponder];
2427 dispatch_get_main_queue(), ^{
2428 [UIView setAnimationsEnabled:YES];
2429 [
self dismissKeyboardScreenshot];
2433 - (void)handlePointerMove:(CGFloat)pointerY {
2436 CGFloat screenHeight = screen.bounds.size.height;
2437 CGFloat keyboardHeight = _keyboardRect.size.height;
2438 if (screenHeight - keyboardHeight <= pointerY) {
2440 if (_keyboardView.superview == nil) {
2442 [
self takeKeyboardScreenshotAndDisplay];
2443 [
self hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate];
2445 [
self setKeyboardContainerHeight:pointerY];
2446 _pointerYVelocity = _previousPointerYPosition - pointerY;
2449 if (_keyboardView.superview != nil) {
2451 _keyboardViewContainer.frame = _keyboardRect;
2452 _pointerYVelocity = _previousPointerYPosition - pointerY;
2455 _previousPointerYPosition = pointerY;
2458 - (void)setKeyboardContainerHeight:(CGFloat)pointerY {
2459 CGRect frameRect = _keyboardRect;
2460 frameRect.origin.y = pointerY;
2461 _keyboardViewContainer.frame = frameRect;
2464 - (void)hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate {
2465 [UIView setAnimationsEnabled:NO];
2466 _cachedFirstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
2467 _activeView.preventCursorDismissWhenResignFirstResponder = YES;
2468 [_cachedFirstResponder resignFirstResponder];
2469 _activeView.preventCursorDismissWhenResignFirstResponder = NO;
2470 [UIView setAnimationsEnabled:YES];
2473 - (void)takeKeyboardScreenshotAndDisplay {
2476 UIView* keyboardSnap = [screen snapshotViewAfterScreenUpdates:YES];
2477 keyboardSnap = [keyboardSnap resizableSnapshotViewFromRect:_keyboardRect
2478 afterScreenUpdates:YES
2479 withCapInsets:UIEdgeInsetsZero];
2480 _keyboardView = keyboardSnap;
2481 [_keyboardViewContainer addSubview:_keyboardView];
2482 if (_keyboardViewContainer.superview == nil) {
2483 [UIApplication.sharedApplication.delegate.window.rootViewController.view
2484 addSubview:_keyboardViewContainer];
2486 _keyboardViewContainer.layer.zPosition = NSIntegerMax;
2487 _keyboardViewContainer.frame = _keyboardRect;
2490 - (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
2491 NSArray* transform = dictionary[@"transform"];
2492 [_activeView setEditableTransform:transform];
2493 const int leftIndex = 12;
2494 const int topIndex = 13;
2498 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue],
2499 [dictionary[
@"width"] intValue], [dictionary[
@"height"] intValue]);
2501 CGRectMake(0, 0, [dictionary[
@"width"] intValue], [dictionary[
@"height"] intValue]);
2502 _activeView.tintColor = [UIColor clearColor];
2507 if (@available(iOS 17, *)) {
2515 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue], 0, 0);
2520 - (void)updateMarkedRect:(NSDictionary*)dictionary {
2521 NSAssert(dictionary[
@"x"] != nil && dictionary[
@"y"] != nil && dictionary[
@"width"] != nil &&
2522 dictionary[
@"height"] != nil,
2523 @"Expected a dictionary representing a CGRect, got %@", dictionary);
2524 CGRect rect = CGRectMake([dictionary[
@"x"] doubleValue], [dictionary[
@"y"] doubleValue],
2525 [dictionary[
@"width"] doubleValue], [dictionary[
@"height"] doubleValue]);
2526 _activeView.markedRect = rect.size.width < 0 && rect.size.height < 0 ?
kInvalidFirstRect : rect;
2529 - (void)setSelectionRects:(NSArray*)encodedRects {
2530 NSMutableArray<FlutterTextSelectionRect*>* rectsAsRect =
2531 [[NSMutableArray alloc] initWithCapacity:[encodedRects count]];
2532 for (NSUInteger i = 0; i < [encodedRects count]; i++) {
2533 NSArray<NSNumber*>* encodedRect = encodedRects[i];
2535 selectionRectWithRect:CGRectMake([encodedRect[0] floatValue],
2536 [encodedRect[1] floatValue],
2537 [encodedRect[2] floatValue],
2538 [encodedRect[3] floatValue])
2539 position:[encodedRect[4] unsignedIntegerValue]
2540 writingDirection:[encodedRect[5] unsignedIntegerValue] == 1
2541 ? NSWritingDirectionLeftToRight
2542 : NSWritingDirectionRightToLeft]];
2548 _activeView.selectionRects = rectsAsRect;
2551 - (void)startLiveTextInput {
2552 if (@available(iOS 15.0, *)) {
2553 if (_activeView == nil || !_activeView.isFirstResponder) {
2556 [_activeView captureTextFromCamera:nil];
2560 - (void)showTextInput {
2561 _activeView.viewResponder = _viewResponder;
2562 [
self addToInputParentViewIfNeeded:_activeView];
2571 if (!_enableFlutterTextInputViewAccessibilityTimer) {
2572 _enableFlutterTextInputViewAccessibilityTimer =
2573 [NSTimer scheduledTimerWithTimeInterval:kUITextInputAccessibilityEnablingDelaySeconds
2575 selector:@selector(enableActiveViewAccessibility)
2579 [_activeView becomeFirstResponder];
2582 - (void)enableActiveViewAccessibility {
2583 if (_activeView.isFirstResponder) {
2584 _activeView.accessibilityEnabled = YES;
2586 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2589 - (void)hideTextInput {
2590 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2591 _activeView.accessibilityEnabled = NO;
2592 [_activeView resignFirstResponder];
2593 [_activeView removeFromSuperview];
2594 [_inputHider removeFromSuperview];
2597 - (void)triggerAutofillSave:(BOOL)saveEntries {
2598 [_activeView resignFirstResponder];
2603 [
self cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
2604 [_autofillContext removeAllObjects];
2605 [
self changeInputViewsAutofillVisibility:YES];
2607 [_autofillContext removeAllObjects];
2610 [
self cleanUpViewHierarchy:YES clearText:!saveEntries delayRemoval:NO];
2611 [
self addToInputParentViewIfNeeded:_activeView];
2614 - (void)setPlatformViewTextInputClient {
2618 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2619 _activeView.accessibilityEnabled = NO;
2620 [_activeView removeFromSuperview];
2621 [_inputHider removeFromSuperview];
2624 - (void)setTextInputClient:(
int)client withConfiguration:(NSDictionary*)configuration {
2625 [
self resetAllClientIds];
2628 [
self changeInputViewsAutofillVisibility:NO];
2632 case kFlutterAutofillTypeNone:
2633 self.activeView = [
self createInputViewWith:configuration];
2635 case kFlutterAutofillTypeRegular:
2638 self.activeView = [
self updateAndShowAutofillViews:nil
2639 focusedField:configuration
2640 isPasswordRelated:NO];
2642 case kFlutterAutofillTypePassword:
2643 self.activeView = [
self updateAndShowAutofillViews:configuration[kAssociatedAutofillFields]
2644 focusedField:configuration
2645 isPasswordRelated:YES];
2648 [_activeView setTextInputClient:client];
2649 [_activeView reloadInputViews];
2661 [
self cleanUpViewHierarchy:NO clearText:YES delayRemoval:YES];
2672 [_autofillContext removeObjectForKey:autofillId];
2675 [newView configureWithDictionary:configuration];
2676 [
self addToInputParentViewIfNeeded:newView];
2680 if (autofillId &&
AutofillTypeOf(field) == kFlutterAutofillTypeNone) {
2681 [_autofillContext removeObjectForKey:autofillId];
2688 focusedField:(NSDictionary*)focusedField
2689 isPasswordRelated:(BOOL)isPassword {
2692 NSAssert(focusedId,
@"autofillId must not be null for the focused field: %@", focusedField);
2697 focused = [
self getOrCreateAutofillableView:focusedField isPasswordAutofill:isPassword];
2698 [_autofillContext removeObjectForKey:focusedId];
2701 for (NSDictionary* field in fields) {
2703 NSAssert(autofillId,
@"autofillId must not be null for field: %@", field);
2705 BOOL hasHints =
AutofillTypeOf(field) != kFlutterAutofillTypeNone;
2706 BOOL isFocused = [focusedId isEqualToString:autofillId];
2709 focused = [
self getOrCreateAutofillableView:field isPasswordAutofill:isPassword];
2714 _autofillContext[autofillId] = isFocused ? focused
2715 : [
self getOrCreateAutofillableView:field
2716 isPasswordAutofill:isPassword];
2719 [_autofillContext removeObjectForKey:autofillId];
2723 NSAssert(focused,
@"The current focused input view must not be nil.");
2733 isPasswordAutofill:(BOOL)needsPasswordAutofill {
2739 inputView = [inputView initWithOwner:self];
2740 [
self addToInputParentViewIfNeeded:inputView];
2743 [inputView configureWithDictionary:field];
2748 - (UIView*)hostView {
2750 NSAssert(host !=
nullptr,
2751 @"The application must have a host view since the keyboard client "
2752 @"must be part of the responder chain to function. The host view controller is %@",
2758 - (NSArray<UIView*>*)textInputViews {
2759 return _inputHider.subviews;
2772 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
2773 clearText:(BOOL)clearText
2774 delayRemoval:(BOOL)delayRemoval {
2775 for (UIView* view in
self.textInputViews) {
2777 (includeActiveView || view != _activeView)) {
2779 if (_autofillContext[inputView.autofillId] != view) {
2781 [inputView replaceRangeLocal:NSMakeRange(0, inputView.text.length) withText:@""];
2784 [inputView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:0.1];
2786 [inputView removeFromSuperview];
2795 - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
2796 for (UIView* view in
self.textInputViews) {
2799 inputView.isVisibleToAutofill = newVisibility;
2811 - (void)resetAllClientIds {
2812 for (UIView* view in
self.textInputViews) {
2815 [inputView setTextInputClient:0];
2821 if (![inputView isDescendantOfView:_inputHider]) {
2822 [_inputHider addSubview:inputView];
2831 UIView* parentView =
self.hostView;
2832 if (_inputHider.superview != parentView) {
2833 [parentView addSubview:_inputHider];
2837 - (void)setTextInputEditingState:(NSDictionary*)state {
2838 [_activeView setTextInputState:state];
2841 - (void)clearTextInputClient {
2842 [_activeView setTextInputClient:0];
2843 _activeView.frame = CGRectZero;
2846 - (void)updateConfig:(NSDictionary*)dictionary {
2847 BOOL isSecureTextEntry = [dictionary[kSecureTextEntry] boolValue];
2848 for (UIView* view in
self.textInputViews) {
2855 if (inputView.isSecureTextEntry != isSecureTextEntry) {
2856 inputView.secureTextEntry = isSecureTextEntry;
2857 [inputView reloadInputViews];
2863 #pragma mark UIIndirectScribbleInteractionDelegate
2865 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2866 isElementFocused:(UIScribbleElementIdentifier)elementIdentifier
2867 API_AVAILABLE(ios(14.0)) {
2868 return _activeView.scribbleFocusStatus == FlutterScribbleFocusStatusFocused;
2871 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2872 focusElementIfNeeded:(UIScribbleElementIdentifier)elementIdentifier
2873 referencePoint:(CGPoint)focusReferencePoint
2874 completion:(
void (^)(UIResponder<UITextInput>* focusedInput))completion
2875 API_AVAILABLE(ios(14.0)) {
2876 _activeView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
2877 [_indirectScribbleDelegate flutterTextInputPlugin:self
2878 focusElement:elementIdentifier
2879 atPoint:focusReferencePoint
2880 result:^(id _Nullable result) {
2881 _activeView.scribbleFocusStatus =
2882 FlutterScribbleFocusStatusFocused;
2883 completion(_activeView);
2887 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2888 shouldDelayFocusForElement:(UIScribbleElementIdentifier)elementIdentifier
2889 API_AVAILABLE(ios(14.0)) {
2893 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2894 willBeginWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
2895 API_AVAILABLE(ios(14.0)) {
2898 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2899 didFinishWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
2900 API_AVAILABLE(ios(14.0)) {
2903 - (CGRect)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2904 frameForElement:(UIScribbleElementIdentifier)elementIdentifier
2905 API_AVAILABLE(ios(14.0)) {
2906 NSValue* elementValue = [_scribbleElements objectForKey:elementIdentifier];
2907 if (elementValue == nil) {
2910 return [elementValue CGRectValue];
2913 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2914 requestElementsInRect:(CGRect)rect
2916 (
void (^)(NSArray<UIScribbleElementIdentifier>* elements))completion
2917 API_AVAILABLE(ios(14.0)) {
2918 [_indirectScribbleDelegate
2919 flutterTextInputPlugin:self
2920 requestElementsInRect:rect
2921 result:^(id _Nullable result) {
2922 NSMutableArray<UIScribbleElementIdentifier>* elements =
2923 [[NSMutableArray alloc] init];
2924 if ([result isKindOfClass:[NSArray class]]) {
2925 for (NSArray* elementArray in result) {
2926 [elements addObject:elementArray[0]];
2929 valueWithCGRect:CGRectMake(
2930 [elementArray[1] floatValue],
2931 [elementArray[2] floatValue],
2932 [elementArray[3] floatValue],
2933 [elementArray[4] floatValue])]
2934 forKey:elementArray[0]];
2937 completion(elements);
2941 #pragma mark - Methods related to Scribble support
2945 if (@available(iOS 14.0, *)) {
2947 if (parentView != nil) {
2948 UIIndirectScribbleInteraction* scribbleInteraction = [[UIIndirectScribbleInteraction alloc]
2949 initWithDelegate:(id<UIIndirectScribbleInteractionDelegate>)self];
2950 [parentView addInteraction:scribbleInteraction];
2957 - (void)resetViewResponder {
2958 _viewResponder = nil;
2962 #pragma mark FlutterKeySecondaryResponder
2977 - (
id)flutterFirstResponder {
2978 if (
self.isFirstResponder) {
2981 for (UIView* subView in
self.subviews) {
2982 UIView* firstResponder = subView.flutterFirstResponder;
2983 if (firstResponder) {
2984 return firstResponder;