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 if ([inputType isEqualToString:
@"TextInputType.visiblePassword"]) {
141 return UIKeyboardTypeASCIICapable;
143 if ([inputType isEqualToString:
@"TextInputType.webSearch"]) {
144 return UIKeyboardTypeWebSearch;
146 if ([inputType isEqualToString:
@"TextInputType.twitter"]) {
147 return UIKeyboardTypeTwitter;
149 return UIKeyboardTypeDefault;
153 NSString* textCapitalization = type[
@"textCapitalization"];
154 if ([textCapitalization isEqualToString:
@"TextCapitalization.characters"]) {
155 return UITextAutocapitalizationTypeAllCharacters;
156 }
else if ([textCapitalization isEqualToString:
@"TextCapitalization.sentences"]) {
157 return UITextAutocapitalizationTypeSentences;
158 }
else if ([textCapitalization isEqualToString:
@"TextCapitalization.words"]) {
159 return UITextAutocapitalizationTypeWords;
161 return UITextAutocapitalizationTypeNone;
169 if ([inputType isEqualToString:
@"TextInputAction.unspecified"]) {
170 return UIReturnKeyDefault;
173 if ([inputType isEqualToString:
@"TextInputAction.done"]) {
174 return UIReturnKeyDone;
177 if ([inputType isEqualToString:
@"TextInputAction.go"]) {
178 return UIReturnKeyGo;
181 if ([inputType isEqualToString:
@"TextInputAction.send"]) {
182 return UIReturnKeySend;
185 if ([inputType isEqualToString:
@"TextInputAction.search"]) {
186 return UIReturnKeySearch;
189 if ([inputType isEqualToString:
@"TextInputAction.next"]) {
190 return UIReturnKeyNext;
193 if ([inputType isEqualToString:
@"TextInputAction.continueAction"]) {
194 return UIReturnKeyContinue;
197 if ([inputType isEqualToString:
@"TextInputAction.join"]) {
198 return UIReturnKeyJoin;
201 if ([inputType isEqualToString:
@"TextInputAction.route"]) {
202 return UIReturnKeyRoute;
205 if ([inputType isEqualToString:
@"TextInputAction.emergencyCall"]) {
206 return UIReturnKeyEmergencyCall;
209 if ([inputType isEqualToString:
@"TextInputAction.newline"]) {
210 return UIReturnKeyDefault;
214 return UIReturnKeyDefault;
218 if (!hints || hints.count == 0) {
223 NSString* hint = hints[0];
224 if ([hint isEqualToString:
@"addressCityAndState"]) {
225 return UITextContentTypeAddressCityAndState;
228 if ([hint isEqualToString:
@"addressState"]) {
229 return UITextContentTypeAddressState;
232 if ([hint isEqualToString:
@"addressCity"]) {
233 return UITextContentTypeAddressCity;
236 if ([hint isEqualToString:
@"sublocality"]) {
237 return UITextContentTypeSublocality;
240 if ([hint isEqualToString:
@"streetAddressLine1"]) {
241 return UITextContentTypeStreetAddressLine1;
244 if ([hint isEqualToString:
@"streetAddressLine2"]) {
245 return UITextContentTypeStreetAddressLine2;
248 if ([hint isEqualToString:
@"countryName"]) {
249 return UITextContentTypeCountryName;
252 if ([hint isEqualToString:
@"fullStreetAddress"]) {
253 return UITextContentTypeFullStreetAddress;
256 if ([hint isEqualToString:
@"postalCode"]) {
257 return UITextContentTypePostalCode;
260 if ([hint isEqualToString:
@"location"]) {
261 return UITextContentTypeLocation;
264 if ([hint isEqualToString:
@"creditCardNumber"]) {
265 return UITextContentTypeCreditCardNumber;
268 if ([hint isEqualToString:
@"email"]) {
269 return UITextContentTypeEmailAddress;
272 if ([hint isEqualToString:
@"jobTitle"]) {
273 return UITextContentTypeJobTitle;
276 if ([hint isEqualToString:
@"givenName"]) {
277 return UITextContentTypeGivenName;
280 if ([hint isEqualToString:
@"middleName"]) {
281 return UITextContentTypeMiddleName;
284 if ([hint isEqualToString:
@"familyName"]) {
285 return UITextContentTypeFamilyName;
288 if ([hint isEqualToString:
@"name"]) {
289 return UITextContentTypeName;
292 if ([hint isEqualToString:
@"namePrefix"]) {
293 return UITextContentTypeNamePrefix;
296 if ([hint isEqualToString:
@"nameSuffix"]) {
297 return UITextContentTypeNameSuffix;
300 if ([hint isEqualToString:
@"nickname"]) {
301 return UITextContentTypeNickname;
304 if ([hint isEqualToString:
@"organizationName"]) {
305 return UITextContentTypeOrganizationName;
308 if ([hint isEqualToString:
@"telephoneNumber"]) {
309 return UITextContentTypeTelephoneNumber;
312 if ([hint isEqualToString:
@"password"]) {
313 return UITextContentTypePassword;
316 if ([hint isEqualToString:
@"oneTimeCode"]) {
317 return UITextContentTypeOneTimeCode;
320 if ([hint isEqualToString:
@"newPassword"]) {
321 return UITextContentTypeNewPassword;
389 typedef NS_ENUM(NSInteger, FlutterAutofillType) {
393 kFlutterAutofillTypeNone,
394 kFlutterAutofillTypeRegular,
395 kFlutterAutofillTypePassword,
405 if (isSecureTextEntry) {
412 if ([contentType isEqualToString:UITextContentTypePassword] ||
413 [contentType isEqualToString:UITextContentTypeUsername]) {
417 if ([contentType isEqualToString:UITextContentTypeNewPassword]) {
427 return kFlutterAutofillTypePassword;
432 return kFlutterAutofillTypePassword;
437 return !autofill || [contentType isEqualToString:
@""] ? kFlutterAutofillTypeNone
438 : kFlutterAutofillTypeRegular;
442 return fabsf(x - y) <= delta;
468 CGRect selectionRect,
469 BOOL selectionRectIsRTL,
470 BOOL useTrailingBoundaryOfSelectionRect,
471 CGRect otherSelectionRect,
472 BOOL otherSelectionRectIsRTL,
473 CGFloat verticalPrecision) {
475 if (CGRectContainsPoint(
477 selectionRect.origin.x + ((useTrailingBoundaryOfSelectionRect ^ selectionRectIsRTL)
478 ? 0.5 * selectionRect.size.width
480 selectionRect.origin.y, 0.5 * selectionRect.size.width, selectionRect.size.height),
485 CGPoint pointForSelectionRect = CGPointMake(
486 selectionRect.origin.x +
487 (selectionRectIsRTL ^ useTrailingBoundaryOfSelectionRect ? selectionRect.size.width : 0),
488 selectionRect.origin.y + selectionRect.size.height * 0.5);
489 float yDist = fabs(pointForSelectionRect.y - point.y);
490 float xDist = fabs(pointForSelectionRect.x - point.x);
493 CGPoint pointForOtherSelectionRect = CGPointMake(
494 otherSelectionRect.origin.x + (otherSelectionRectIsRTL ? otherSelectionRect.size.width : 0),
495 otherSelectionRect.origin.y + otherSelectionRect.size.height * 0.5);
496 float yDistOther = fabs(pointForOtherSelectionRect.y - point.y);
497 float xDistOther = fabs(pointForOtherSelectionRect.x - point.x);
502 BOOL isCloserVertically = yDist < yDistOther - verticalPrecision;
504 BOOL isAboveBottomOfLine = point.y <= selectionRect.origin.y + selectionRect.size.height;
505 BOOL isCloserHorizontally = xDist < xDistOther;
506 BOOL isBelowBottomOfLine = point.y > selectionRect.origin.y + selectionRect.size.height;
509 if (selectionRectIsRTL) {
510 isFarther = selectionRect.origin.x < otherSelectionRect.origin.x;
512 isFarther = selectionRect.origin.x +
513 (useTrailingBoundaryOfSelectionRect ? selectionRect.size.width : 0) >
514 otherSelectionRect.origin.x;
516 return (isCloserVertically ||
517 (isEqualVertically &&
518 ((isAboveBottomOfLine && isCloserHorizontally) || (isBelowBottomOfLine && isFarther))));
521 #pragma mark - FlutterTextPosition
525 + (instancetype)positionWithIndex:(NSUInteger)index {
526 return [[
FlutterTextPosition alloc] initWithIndex:index affinity:UITextStorageDirectionForward];
529 + (instancetype)positionWithIndex:(NSUInteger)index affinity:(UITextStorageDirection)affinity {
533 - (instancetype)initWithIndex:(NSUInteger)index affinity:(UITextStorageDirection)affinity {
544 #pragma mark - FlutterTextRange
548 + (instancetype)rangeWithNSRange:(NSRange)range {
552 - (instancetype)initWithNSRange:(NSRange)range {
560 - (UITextPosition*)start {
562 affinity:UITextStorageDirectionForward];
565 - (UITextPosition*)end {
567 affinity:UITextStorageDirectionBackward];
571 return self.range.length == 0;
574 - (id)copyWithZone:(NSZone*)zone {
579 return NSEqualRanges(
self.
range, other.
range);
583 #pragma mark - FlutterTokenizer
593 - (instancetype)initWithTextInput:(UIResponder<UITextInput>*)textInput {
595 @"The FlutterTokenizer can only be used in a FlutterTextInputView");
596 self = [
super initWithTextInput:textInput];
603 - (UITextRange*)rangeEnclosingPosition:(UITextPosition*)position
604 withGranularity:(UITextGranularity)granularity
605 inDirection:(UITextDirection)direction {
607 switch (granularity) {
608 case UITextGranularityLine:
611 result = [
self lineEnclosingPosition:position inDirection:direction];
613 case UITextGranularityCharacter:
614 case UITextGranularityWord:
615 case UITextGranularitySentence:
616 case UITextGranularityParagraph:
617 case UITextGranularityDocument:
619 result = [
super rangeEnclosingPosition:position
620 withGranularity:granularity
621 inDirection:direction];
627 - (UITextRange*)lineEnclosingPosition:(UITextPosition*)position
628 inDirection:(UITextDirection)direction {
630 if (@available(iOS 17.0, *)) {
635 if (flutterPosition.
index > _textInputView.text.length ||
636 (flutterPosition.
index == _textInputView.text.length &&
637 direction == UITextStorageDirectionForward)) {
643 NSString* textAfter = [_textInputView
644 textInRange:[_textInputView textRangeFromPosition:position
645 toPosition:[_textInputView endOfDocument]]];
646 NSArray<NSString*>* linesAfter = [textAfter componentsSeparatedByString:@"\n"];
647 NSInteger offSetToLineBreak = [linesAfter firstObject].length;
648 UITextPosition* lineBreakAfter = [_textInputView positionFromPosition:position
649 offset:offSetToLineBreak];
651 NSString* textBefore = [_textInputView
652 textInRange:[_textInputView textRangeFromPosition:[_textInputView beginningOfDocument]
653 toPosition:position]];
654 NSArray<NSString*>* linesBefore = [textBefore componentsSeparatedByString:@"\n"];
655 NSInteger offSetFromLineBreak = [linesBefore lastObject].length;
656 UITextPosition* lineBreakBefore = [_textInputView positionFromPosition:position
657 offset:-offSetFromLineBreak];
659 return [_textInputView textRangeFromPosition:lineBreakBefore toPosition:lineBreakAfter];
664 #pragma mark - FlutterTextSelectionRect
669 @synthesize rect = _rect;
675 + (instancetype)selectionRectWithRectAndInfo:(CGRect)rect
676 position:(NSUInteger)position
677 writingDirection:(NSWritingDirection)writingDirection
678 containsStart:(BOOL)containsStart
679 containsEnd:(BOOL)containsEnd
680 isVertical:(BOOL)isVertical {
683 writingDirection:writingDirection
684 containsStart:containsStart
685 containsEnd:containsEnd
686 isVertical:isVertical];
689 + (instancetype)selectionRectWithRect:(CGRect)rect position:(NSUInteger)position {
692 writingDirection:NSWritingDirectionNatural
698 + (instancetype)selectionRectWithRect:(CGRect)rect
699 position:(NSUInteger)position
700 writingDirection:(NSWritingDirection)writingDirection {
703 writingDirection:writingDirection
709 - (instancetype)initWithRectAndInfo:(CGRect)rect
710 position:(NSUInteger)position
711 writingDirection:(NSWritingDirection)writingDirection
712 containsStart:(BOOL)containsStart
713 containsEnd:(BOOL)containsEnd
714 isVertical:(BOOL)isVertical {
728 return _writingDirection == NSWritingDirectionRightToLeft;
733 #pragma mark - FlutterTextPlaceholder
737 - (NSArray<UITextSelectionRect*>*)rects {
753 @property(nonatomic, retain, readonly) UITextField*
textField;
757 UITextField* _textField;
762 _textField = [[UITextField alloc] init];
767 - (BOOL)isKindOfClass:(Class)aClass {
768 return [
super isKindOfClass:aClass] || (aClass == [UITextField class]);
771 - (NSMethodSignature*)methodSignatureForSelector:(
SEL)aSelector {
772 NSMethodSignature* signature = [
super methodSignatureForSelector:aSelector];
774 signature = [
self.textField methodSignatureForSelector:aSelector];
779 - (void)forwardInvocation:(NSInvocation*)anInvocation {
780 [anInvocation invokeWithTarget:self.textField];
786 @property(nonatomic, readonly, weak) id<FlutterTextInputDelegate> textInputDelegate;
787 @property(nonatomic, readonly) UIView* hostView;
792 @property(nonatomic, copy) NSString* autofillId;
793 @property(nonatomic, readonly) CATransform3D editableTransform;
794 @property(nonatomic, assign) CGRect markedRect;
796 @property(nonatomic, assign) BOOL preventCursorDismissWhenResignFirstResponder;
797 @property(nonatomic) BOOL isVisibleToAutofill;
798 @property(nonatomic, assign) BOOL accessibilityEnabled;
799 @property(nonatomic, assign)
int textInputClient;
803 @property(nonatomic, copy) NSString* temporarilyDeletedComposedCharacter;
804 @property(nonatomic, assign) CGRect editMenuTargetRect;
806 - (void)setEditableTransform:(NSArray*)matrix;
810 int _textInputClient;
824 UITextInteraction* _textInteraction
API_AVAILABLE(ios(13.0));
827 @synthesize tokenizer = _tokenizer;
830 self = [
super initWithFrame:CGRectZero];
833 _textInputClient = 0;
835 _preventCursorDismissWhenResignFirstResponder = NO;
838 _text = [[NSMutableString alloc] init];
843 _pendingDeltas = [[NSMutableArray alloc] init];
846 _editableTransform = CATransform3D();
849 _autocapitalizationType = UITextAutocapitalizationTypeSentences;
850 _autocorrectionType = UITextAutocorrectionTypeDefault;
851 _spellCheckingType = UITextSpellCheckingTypeDefault;
852 _enablesReturnKeyAutomatically = NO;
853 _keyboardAppearance = UIKeyboardAppearanceDefault;
854 _keyboardType = UIKeyboardTypeDefault;
855 _returnKeyType = UIReturnKeyDone;
856 _secureTextEntry = NO;
857 _enableDeltaModel = NO;
859 _accessibilityEnabled = NO;
860 _smartQuotesType = UITextSmartQuotesTypeYes;
861 _smartDashesType = UITextSmartDashesTypeYes;
862 _selectionRects = [[NSArray alloc] init];
864 if (@available(iOS 14.0, *)) {
865 UIScribbleInteraction* interaction = [[UIScribbleInteraction alloc] initWithDelegate:self];
866 [
self addInteraction:interaction];
870 if (@available(iOS 16.0, *)) {
871 _editMenuInteraction = [[UIEditMenuInteraction alloc] initWithDelegate:self];
872 [
self addInteraction:_editMenuInteraction];
878 - (UIMenu*)editMenuInteraction:(UIEditMenuInteraction*)interaction
879 menuForConfiguration:(UIEditMenuConfiguration*)configuration
880 suggestedActions:(NSArray<UIMenuElement*>*)suggestedActions API_AVAILABLE(ios(16.0)) {
881 return [UIMenu menuWithChildren:suggestedActions];
884 - (void)editMenuInteraction:(UIEditMenuInteraction*)interaction
885 willDismissMenuForConfiguration:(UIEditMenuConfiguration*)configuration
886 animator:(
id<UIEditMenuInteractionAnimating>)animator
887 API_AVAILABLE(ios(16.0)) {
888 [
self.textInputDelegate flutterTextInputView:self
889 willDismissEditMenuWithTextInputClient:_textInputClient];
892 - (CGRect)editMenuInteraction:(UIEditMenuInteraction*)interaction
893 targetRectForConfiguration:(UIEditMenuConfiguration*)configuration API_AVAILABLE(ios(16.0)) {
894 return _editMenuTargetRect;
897 - (void)showEditMenuWithTargetRect:(CGRect)targetRect API_AVAILABLE(ios(16.0)) {
898 _editMenuTargetRect = targetRect;
899 UIEditMenuConfiguration* config =
900 [UIEditMenuConfiguration configurationWithIdentifier:nil sourcePoint:CGPointZero];
901 [
self.editMenuInteraction presentEditMenuWithConfiguration:config];
905 [
self.editMenuInteraction dismissMenu];
908 - (void)configureWithDictionary:(NSDictionary*)configuration {
909 NSDictionary* inputType = configuration[kKeyboardType];
911 NSDictionary* autofill = configuration[kAutofillProperties];
913 self.secureTextEntry = [configuration[kSecureTextEntry] boolValue];
914 self.enableDeltaModel = [configuration[kEnableDeltaModel] boolValue];
921 NSString* smartDashesType = configuration[kSmartDashesType];
923 bool smartDashesIsDisabled = smartDashesType && [smartDashesType isEqualToString:@"0"];
924 self.smartDashesType = smartDashesIsDisabled ? UITextSmartDashesTypeNo : UITextSmartDashesTypeYes;
925 NSString* smartQuotesType = configuration[kSmartQuotesType];
927 bool smartQuotesIsDisabled = smartQuotesType && [smartQuotesType isEqualToString:@"0"];
928 self.smartQuotesType = smartQuotesIsDisabled ? UITextSmartQuotesTypeNo : UITextSmartQuotesTypeYes;
930 self.keyboardAppearance = UIKeyboardAppearanceDark;
932 self.keyboardAppearance = UIKeyboardAppearanceLight;
934 self.keyboardAppearance = UIKeyboardAppearanceDefault;
936 NSString* autocorrect = configuration[kAutocorrectionType];
937 bool autocorrectIsDisabled = autocorrect && ![autocorrect boolValue];
938 self.autocorrectionType =
939 autocorrectIsDisabled ? UITextAutocorrectionTypeNo : UITextAutocorrectionTypeDefault;
940 self.spellCheckingType =
941 autocorrectIsDisabled ? UITextSpellCheckingTypeNo : UITextSpellCheckingTypeDefault;
943 if (autofill == nil) {
944 self.textContentType =
@"";
947 [
self setTextInputState:autofill[kAutofillEditingValue]];
948 NSAssert(_autofillId,
@"The autofill configuration must contain an autofill id");
952 self.isVisibleToAutofill = autofill || _secureTextEntry;
955 - (UITextContentType)textContentType {
956 return _textContentType;
969 - (UIColor*)insertionPointColor {
970 return [UIColor clearColor];
973 - (UIColor*)selectionBarColor {
974 return [UIColor clearColor];
977 - (UIColor*)selectionHighlightColor {
978 return [UIColor clearColor];
981 - (UIInputViewController*)inputViewController {
993 return _textInputPlugin.textInputDelegate;
996 - (BOOL)respondsToSelector:(
SEL)selector {
997 if (@available(iOS 17.0, *)) {
999 if (selector ==
@selector(insertionPointColor)) {
1003 return [
super respondsToSelector:selector];
1006 - (void)setTextInputClient:(
int)client {
1007 _textInputClient = client;
1011 - (UITextInteraction*)textInteraction
API_AVAILABLE(ios(13.0)) {
1012 if (!_textInteraction) {
1013 _textInteraction = [UITextInteraction textInteractionForMode:UITextInteractionModeEditable];
1014 _textInteraction.textInput =
self;
1016 return _textInteraction;
1019 - (void)setTextInputState:(NSDictionary*)state {
1020 if (@available(iOS 13.0, *)) {
1027 [
self addInteraction:self.textInteraction];
1031 NSString* newText = state[@"text"];
1032 BOOL textChanged = ![
self.text isEqualToString:newText];
1034 [
self.inputDelegate textWillChange:self];
1035 [
self.text setString:newText];
1037 NSInteger composingBase = [state[@"composingBase"] intValue];
1038 NSInteger composingExtent = [state[@"composingExtent"] intValue];
1039 NSRange composingRange = [
self clampSelection:NSMakeRange(MIN(composingBase, composingExtent),
1040 ABS(composingBase - composingExtent))
1043 self.markedTextRange =
1046 NSRange selectedRange = [
self clampSelectionFromBase:[state[@"selectionBase"] intValue]
1047 extent:[state[@"selectionExtent"] intValue]
1050 NSRange oldSelectedRange = [(
FlutterTextRange*)
self.selectedTextRange range];
1051 if (!NSEqualRanges(selectedRange, oldSelectedRange)) {
1052 [
self.inputDelegate selectionWillChange:self];
1060 [
self.inputDelegate selectionDidChange:self];
1064 [
self.inputDelegate textDidChange:self];
1067 if (@available(iOS 13.0, *)) {
1068 if (_textInteraction) {
1069 [
self removeInteraction:_textInteraction];
1075 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
1076 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1077 [
self resetScribbleInteractionStatusIfEnding];
1078 [
self.viewResponder touchesBegan:touches withEvent:event];
1081 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
1082 [
self.viewResponder touchesMoved:touches withEvent:event];
1085 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
1086 [
self.viewResponder touchesEnded:touches withEvent:event];
1089 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1090 [
self.viewResponder touchesCancelled:touches withEvent:event];
1093 - (void)touchesEstimatedPropertiesUpdated:(NSSet*)touches {
1094 [
self.viewResponder touchesEstimatedPropertiesUpdated:touches];
1104 - (NSRange)clampSelectionFromBase:(
int)selectionBase
1105 extent:(
int)selectionExtent
1106 forText:(NSString*)text {
1107 int loc = MIN(selectionBase, selectionExtent);
1108 int len = ABS(selectionExtent - selectionBase);
1109 return loc < 0 ? NSMakeRange(0, 0)
1110 : [
self clampSelection:NSMakeRange(loc, len) forText:
self.text];
1113 - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text {
1114 NSUInteger start = MIN(MAX(range.location, 0), text.length);
1115 NSUInteger length = MIN(range.length, text.length - start);
1116 return NSMakeRange(start, length);
1119 - (BOOL)isVisibleToAutofill {
1120 return self.frame.size.width > 0 &&
self.frame.size.height > 0;
1128 - (void)setIsVisibleToAutofill:(BOOL)isVisibleToAutofill {
1131 self.frame = isVisibleToAutofill ? CGRectMake(0, 0, 1, 1) : CGRectZero;
1134 #pragma mark UIScribbleInteractionDelegate
1139 if (@available(iOS 14.0, *)) {
1140 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
1147 - (void)scribbleInteractionWillBeginWriting:(UIScribbleInteraction*)interaction
1148 API_AVAILABLE(ios(14.0)) {
1150 [
self.textInputDelegate flutterTextInputViewScribbleInteractionBegan:self];
1153 - (void)scribbleInteractionDidFinishWriting:(UIScribbleInteraction*)interaction
1154 API_AVAILABLE(ios(14.0)) {
1156 [
self.textInputDelegate flutterTextInputViewScribbleInteractionFinished:self];
1159 - (BOOL)scribbleInteraction:(UIScribbleInteraction*)interaction
1160 shouldBeginAtLocation:(CGPoint)location API_AVAILABLE(ios(14.0)) {
1164 - (BOOL)scribbleInteractionShouldDelayFocus:(UIScribbleInteraction*)interaction
1165 API_AVAILABLE(ios(14.0)) {
1169 #pragma mark - UIResponder Overrides
1171 - (BOOL)canBecomeFirstResponder {
1176 return _textInputClient != 0;
1179 - (BOOL)resignFirstResponder {
1180 BOOL success = [
super resignFirstResponder];
1182 if (!_preventCursorDismissWhenResignFirstResponder) {
1183 [
self.textInputDelegate flutterTextInputView:self
1184 didResignFirstResponderWithTextInputClient:_textInputClient];
1190 - (BOOL)canPerformAction:(
SEL)action withSender:(
id)sender {
1191 if (action ==
@selector(paste:)) {
1193 return [UIPasteboard generalPasteboard].hasStrings;
1194 }
else if (action ==
@selector(copy:) || action ==
@selector(cut:) ||
1195 action ==
@selector(
delete:)) {
1196 return [
self textInRange:_selectedTextRange].length > 0;
1197 }
else if (action ==
@selector(selectAll:)) {
1198 return self.hasText;
1200 return [
super canPerformAction:action withSender:sender];
1203 #pragma mark - UIResponderStandardEditActions Overrides
1205 - (void)cut:(
id)sender {
1206 [UIPasteboard generalPasteboard].string = [
self textInRange:_selectedTextRange];
1207 [
self replaceRange:_selectedTextRange withText:@""];
1210 - (void)copy:(
id)sender {
1211 [UIPasteboard generalPasteboard].string = [
self textInRange:_selectedTextRange];
1214 - (void)paste:(
id)sender {
1215 NSString* pasteboardString = [UIPasteboard generalPasteboard].string;
1216 if (pasteboardString != nil) {
1217 [
self insertText:pasteboardString];
1221 - (void)delete:(
id)sender {
1222 [
self replaceRange:_selectedTextRange withText:@""];
1225 - (void)selectAll:(
id)sender {
1226 [
self setSelectedTextRange:[
self textRangeFromPosition:[
self beginningOfDocument]
1227 toPosition:[
self endOfDocument]]];
1230 #pragma mark - UITextInput Overrides
1232 - (id<UITextInputTokenizer>)tokenizer {
1233 if (_tokenizer == nil) {
1240 return [_selectedTextRange copy];
1244 - (void)setSelectedTextRangeLocal:(UITextRange*)selectedTextRange {
1249 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, flutterTextRange.range)] copy];
1256 - (void)setSelectedTextRange:(UITextRange*)selectedTextRange {
1261 [
self setSelectedTextRangeLocal:selectedTextRange];
1263 if (_enableDeltaModel) {
1264 [
self updateEditingStateWithDelta:flutter::TextEditingDelta([
self.text UTF8String])];
1266 [
self updateEditingState];
1270 _scribbleFocusStatus == FlutterScribbleFocusStatusFocused) {
1274 if (flutterTextRange.
range.length > 0) {
1275 [
self.textInputDelegate flutterTextInputView:self showToolbar:_textInputClient];
1279 [
self resetScribbleInteractionStatusIfEnding];
1282 - (id)insertDictationResultPlaceholder {
1286 - (void)removeDictationResultPlaceholder:(
id)placeholder willInsertResult:(BOOL)willInsertResult {
1289 - (NSString*)textInRange:(UITextRange*)range {
1294 @"Expected a FlutterTextRange for range (got %@).", [range
class]);
1296 NSAssert(textRange.location != NSNotFound,
@"Expected a valid text range.");
1298 NSUInteger location = MIN(textRange.location,
self.text.length);
1299 NSUInteger length = MIN(
self.text.length - location, textRange.length);
1300 NSRange safeRange = NSMakeRange(location, length);
1301 return [
self.text substringWithRange:safeRange];
1306 - (void)replaceRangeLocal:(NSRange)range withText:(NSString*)text {
1307 [
self.text replaceCharactersInRange:[
self clampSelection:range forText:self.text]
1313 const NSRange newSelectionRange =
1314 [
self clampSelection:NSMakeRange(range.location + text.length, 0) forText:self.text];
1317 self.markedTextRange = nil;
1320 - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
1321 NSString* textBeforeChange = [
self.text copy];
1323 [
self replaceRangeLocal:replaceRange withText:text];
1324 if (_enableDeltaModel) {
1325 NSRange nextReplaceRange = [
self clampSelection:replaceRange forText:textBeforeChange];
1326 [
self updateEditingStateWithDelta:flutter::TextEditingDelta(
1327 [textBeforeChange UTF8String],
1329 nextReplaceRange.location,
1330 nextReplaceRange.location + nextReplaceRange.length),
1331 [text UTF8String])];
1333 [
self updateEditingState];
1337 - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
1340 self.temporarilyDeletedComposedCharacter = nil;
1342 if (
self.
returnKeyType == UIReturnKeyDefault && [text isEqualToString:
@"\n"]) {
1343 [
self.textInputDelegate flutterTextInputView:self
1344 performAction:FlutterTextInputActionNewline
1345 withClient:_textInputClient];
1349 if ([text isEqualToString:
@"\n"]) {
1350 FlutterTextInputAction action;
1352 case UIReturnKeyDefault:
1353 action = FlutterTextInputActionUnspecified;
1355 case UIReturnKeyDone:
1356 action = FlutterTextInputActionDone;
1359 action = FlutterTextInputActionGo;
1361 case UIReturnKeySend:
1362 action = FlutterTextInputActionSend;
1364 case UIReturnKeySearch:
1365 case UIReturnKeyGoogle:
1366 case UIReturnKeyYahoo:
1367 action = FlutterTextInputActionSearch;
1369 case UIReturnKeyNext:
1370 action = FlutterTextInputActionNext;
1372 case UIReturnKeyContinue:
1373 action = FlutterTextInputActionContinue;
1375 case UIReturnKeyJoin:
1376 action = FlutterTextInputActionJoin;
1378 case UIReturnKeyRoute:
1379 action = FlutterTextInputActionRoute;
1381 case UIReturnKeyEmergencyCall:
1382 action = FlutterTextInputActionEmergencyCall;
1386 [
self.textInputDelegate flutterTextInputView:self
1387 performAction:action
1388 withClient:_textInputClient];
1397 - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelectedRange {
1398 NSString* textBeforeChange = [
self.text copy];
1401 _scribbleFocusStatus != FlutterScribbleFocusStatusUnfocused) {
1405 if (markedText == nil) {
1410 const NSRange& actualReplacedRange = currentMarkedTextRange && !currentMarkedTextRange.isEmpty
1411 ? currentMarkedTextRange.
range
1415 [
self.text replaceCharactersInRange:actualReplacedRange withString:markedText];
1417 const NSRange newMarkedRange = NSMakeRange(actualReplacedRange.location, markedText.length);
1418 self.markedTextRange =
1421 [
self setSelectedTextRangeLocal:
1423 rangeWithNSRange:[
self clampSelection:NSMakeRange(markedSelectedRange.location +
1424 newMarkedRange.location,
1425 markedSelectedRange.length)
1426 forText:self.text]]];
1427 if (_enableDeltaModel) {
1428 NSRange nextReplaceRange = [
self clampSelection:actualReplacedRange forText:textBeforeChange];
1429 [
self updateEditingStateWithDelta:flutter::TextEditingDelta(
1430 [textBeforeChange UTF8String],
1432 nextReplaceRange.location,
1433 nextReplaceRange.location + nextReplaceRange.length),
1434 [markedText UTF8String])];
1436 [
self updateEditingState];
1440 - (void)unmarkText {
1444 self.markedTextRange = nil;
1445 if (_enableDeltaModel) {
1446 [
self updateEditingStateWithDelta:flutter::TextEditingDelta([
self.text UTF8String])];
1448 [
self updateEditingState];
1452 - (UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition
1453 toPosition:(UITextPosition*)toPosition {
1456 if (toIndex >= fromIndex) {
1469 - (NSUInteger)decrementOffsetPosition:(NSUInteger)position {
1470 return fml::RangeForCharacterAtIndex(
self.text, MAX(0, position - 1)).location;
1473 - (NSUInteger)incrementOffsetPosition:(NSUInteger)position {
1474 NSRange charRange = fml::RangeForCharacterAtIndex(
self.text, position);
1475 return MIN(position + charRange.length,
self.text.length);
1478 - (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInteger)offset {
1481 NSInteger newLocation = (NSInteger)offsetPosition + offset;
1482 if (newLocation < 0 || newLocation > (NSInteger)
self.text.length) {
1491 for (NSInteger i = 0; i < offset && offsetPosition <
self.text.length; ++i) {
1492 offsetPosition = [
self incrementOffsetPosition:offsetPosition];
1495 for (NSInteger i = 0; i < ABS(offset) && offsetPosition > 0; ++i) {
1496 offsetPosition = [
self decrementOffsetPosition:offsetPosition];
1502 - (UITextPosition*)positionFromPosition:(UITextPosition*)position
1503 inDirection:(UITextLayoutDirection)direction
1504 offset:(NSInteger)offset {
1506 switch (direction) {
1507 case UITextLayoutDirectionLeft:
1508 case UITextLayoutDirectionUp:
1509 return [
self positionFromPosition:position offset:offset * -1];
1510 case UITextLayoutDirectionRight:
1511 case UITextLayoutDirectionDown:
1512 return [
self positionFromPosition:position offset:1];
1516 - (UITextPosition*)beginningOfDocument {
1520 - (UITextPosition*)endOfDocument {
1522 affinity:UITextStorageDirectionBackward];
1525 - (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other {
1528 if (positionIndex < otherIndex) {
1529 return NSOrderedAscending;
1531 if (positionIndex > otherIndex) {
1532 return NSOrderedDescending;
1536 if (positionAffinity == otherAffinity) {
1537 return NSOrderedSame;
1539 if (positionAffinity == UITextStorageDirectionBackward) {
1541 return NSOrderedAscending;
1544 return NSOrderedDescending;
1547 - (NSInteger)offsetFromPosition:(UITextPosition*)from toPosition:(UITextPosition*)toPosition {
1551 - (UITextPosition*)positionWithinRange:(UITextRange*)range
1552 farthestInDirection:(UITextLayoutDirection)direction {
1554 UITextStorageDirection affinity;
1555 switch (direction) {
1556 case UITextLayoutDirectionLeft:
1557 case UITextLayoutDirectionUp:
1559 affinity = UITextStorageDirectionForward;
1561 case UITextLayoutDirectionRight:
1562 case UITextLayoutDirectionDown:
1564 affinity = UITextStorageDirectionBackward;
1570 - (UITextRange*)characterRangeByExtendingPosition:(UITextPosition*)position
1571 inDirection:(UITextLayoutDirection)direction {
1573 NSUInteger startIndex;
1574 NSUInteger endIndex;
1575 switch (direction) {
1576 case UITextLayoutDirectionLeft:
1577 case UITextLayoutDirectionUp:
1578 startIndex = [
self decrementOffsetPosition:positionIndex];
1579 endIndex = positionIndex;
1581 case UITextLayoutDirectionRight:
1582 case UITextLayoutDirectionDown:
1583 startIndex = positionIndex;
1584 endIndex = [
self incrementOffsetPosition:positionIndex];
1590 #pragma mark - UITextInput text direction handling
1592 - (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position
1593 inDirection:(UITextStorageDirection)direction {
1595 return UITextWritingDirectionNatural;
1598 - (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection
1599 forRange:(UITextRange*)range {
1603 #pragma mark - UITextInput cursor, selection rect handling
1605 - (void)setMarkedRect:(CGRect)markedRect {
1606 _markedRect = markedRect;
1613 - (void)setEditableTransform:(NSArray*)matrix {
1614 CATransform3D* transform = &_editableTransform;
1616 transform->m11 = [matrix[0] doubleValue];
1617 transform->m12 = [matrix[1] doubleValue];
1618 transform->m13 = [matrix[2] doubleValue];
1619 transform->m14 = [matrix[3] doubleValue];
1621 transform->m21 = [matrix[4] doubleValue];
1622 transform->m22 = [matrix[5] doubleValue];
1623 transform->m23 = [matrix[6] doubleValue];
1624 transform->m24 = [matrix[7] doubleValue];
1626 transform->m31 = [matrix[8] doubleValue];
1627 transform->m32 = [matrix[9] doubleValue];
1628 transform->m33 = [matrix[10] doubleValue];
1629 transform->m34 = [matrix[11] doubleValue];
1631 transform->m41 = [matrix[12] doubleValue];
1632 transform->m42 = [matrix[13] doubleValue];
1633 transform->m43 = [matrix[14] doubleValue];
1634 transform->m44 = [matrix[15] doubleValue];
1643 CGPoint points[] = {
1644 incomingRect.origin,
1645 CGPointMake(incomingRect.origin.x, incomingRect.origin.y + incomingRect.size.height),
1646 CGPointMake(incomingRect.origin.x + incomingRect.size.width, incomingRect.origin.y),
1647 CGPointMake(incomingRect.origin.x + incomingRect.size.width,
1648 incomingRect.origin.y + incomingRect.size.height)};
1650 CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
1651 CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
1653 for (
int i = 0; i < 4; i++) {
1654 const CGPoint point = points[i];
1656 CGFloat x = _editableTransform.m11 * point.x + _editableTransform.m21 * point.y +
1657 _editableTransform.m41;
1658 CGFloat y = _editableTransform.m12 * point.x + _editableTransform.m22 * point.y +
1659 _editableTransform.m42;
1661 const CGFloat w = _editableTransform.m14 * point.x + _editableTransform.m24 * point.y +
1662 _editableTransform.m44;
1666 }
else if (w != 1.0) {
1671 origin.x = MIN(origin.x, x);
1672 origin.y = MIN(origin.y, y);
1673 farthest.x = MAX(farthest.x, x);
1674 farthest.y = MAX(farthest.y, y);
1676 return CGRectMake(origin.x, origin.y, farthest.x - origin.x, farthest.y - origin.y);
1685 - (CGRect)firstRectForRange:(UITextRange*)range {
1687 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
1689 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
1692 if (_markedTextRange != nil) {
1703 CGRect rect = _markedRect;
1704 if (CGRectIsEmpty(rect)) {
1705 rect = CGRectInset(rect, -0.1, 0);
1710 UIView* hostView = _textInputPlugin.hostView;
1711 NSAssert(hostView == nil || [
self isDescendantOfView:hostView],
@"%@ is not a descendant of %@",
1713 return hostView ? [hostView convertRect:_cachedFirstRect toView:self] :
_cachedFirstRect;
1717 _scribbleFocusStatus == FlutterScribbleFocusStatusUnfocused) {
1718 if (@available(iOS 17.0, *)) {
1728 [
self.textInputDelegate flutterTextInputView:self
1729 showAutocorrectionPromptRectForStart:start
1731 withClient:_textInputClient];
1739 if (@available(iOS 17, *)) {
1745 NSUInteger first = start;
1750 CGRect startSelectionRect = CGRectNull;
1751 CGRect endSelectionRect = CGRectNull;
1754 CGFloat minY = CGFLOAT_MAX;
1755 CGFloat maxY = CGFLOAT_MIN;
1758 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1759 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1760 BOOL startsOnOrBeforeStartOfRange = _selectionRects[i].position <= first;
1761 BOOL isLastSelectionRect = i + 1 == [_selectionRects count];
1762 BOOL endOfTextIsAfterStartOfRange = isLastSelectionRect && textRange.
range.length > first;
1763 BOOL nextSelectionRectIsAfterStartOfRange =
1764 !isLastSelectionRect && _selectionRects[i + 1].position > first;
1765 if (startsOnOrBeforeStartOfRange &&
1766 (endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
1768 if (@available(iOS 17, *)) {
1769 startSelectionRect = _selectionRects[i].rect;
1771 return _selectionRects[i].rect;
1774 if (!CGRectIsNull(startSelectionRect)) {
1775 minY = fmin(minY, CGRectGetMinY(_selectionRects[i].rect));
1776 maxY = fmax(maxY, CGRectGetMaxY(_selectionRects[i].rect));
1777 BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1;
1778 BOOL nextSelectionRectIsOnNextLine =
1779 !isLastSelectionRect &&
1784 CGRectGetMidY(_selectionRects[i + 1].rect) > CGRectGetMaxY(_selectionRects[i].rect);
1785 if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectionRectIsOnNextLine) {
1786 endSelectionRect = _selectionRects[i].rect;
1791 if (CGRectIsNull(startSelectionRect) || CGRectIsNull(endSelectionRect)) {
1795 CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
1796 CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
1797 return CGRectMake(minX, minY, maxX - minX, maxY - minY);
1805 NSArray<UITextSelectionRect*>* rects = [
self
1807 rangeWithNSRange:fml::RangeForCharactersInRange(
1811 (index >= (NSInteger)self.text.length)
1814 if (rects.count == 0) {
1820 CGRect characterAfterCaret = rects[0].rect;
1825 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1826 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1828 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1829 characterAfterCaret.size.height);
1831 }
else if (rects.count == 2 && affinity == UITextStorageDirectionForward) {
1834 CGRect characterAfterCaret = rects[1].rect;
1839 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1840 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1842 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1843 characterAfterCaret.size.height);
1852 CGRect characterBeforeCaret = rects[0].rect;
1855 return CGRectMake(characterBeforeCaret.origin.x, characterBeforeCaret.origin.y, 0,
1856 characterBeforeCaret.size.height);
1858 return CGRectMake(characterBeforeCaret.origin.x + characterBeforeCaret.size.width,
1859 characterBeforeCaret.origin.y, 0, characterBeforeCaret.size.height);
1863 - (UITextPosition*)closestPositionToPoint:(CGPoint)point {
1864 if ([_selectionRects count] == 0) {
1866 @"Expected a FlutterTextPosition for position (got %@).",
1869 UITextStorageDirection currentAffinity =
1875 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1876 return [
self closestPositionToPoint:point withinRange:range];
1879 - (NSArray*)selectionRectsForRange:(UITextRange*)range {
1887 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
1889 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
1892 NSMutableArray* rects = [[NSMutableArray alloc] init];
1893 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1894 if (_selectionRects[i].position >= start &&
1895 (_selectionRects[i].position < end ||
1896 (start == end && _selectionRects[i].position <= end))) {
1897 float width = _selectionRects[i].rect.size.width;
1901 CGRect rect = CGRectMake(_selectionRects[i].rect.origin.x, _selectionRects[i].rect.origin.y,
1902 width, _selectionRects[i].rect.size.height);
1905 position:_selectionRects[i].position
1909 self.text, NSMakeRange(0, self.text.length))
1912 [rects addObject:selectionRect];
1918 - (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range {
1920 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
1922 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
1932 NSUInteger _closestRectIndex = 0;
1933 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1934 NSUInteger position = _selectionRects[i].position;
1935 if (position >= start && position <= end) {
1938 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
1939 NO, _selectionRects[_closestRectIndex].rect,
1940 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
1942 _closestRectIndex = i;
1949 affinity:UITextStorageDirectionForward];
1955 for (NSUInteger i = MAX(0, _closestRectIndex - 1);
1956 i < MIN(_closestRectIndex + 2, [_selectionRects count]); i++) {
1957 NSUInteger position = _selectionRects[i].position + 1;
1958 if (position >= start && position <= end) {
1960 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
1961 YES, _selectionRects[_closestRectIndex].rect,
1962 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
1965 affinity:UITextStorageDirectionBackward];
1970 return closestPosition;
1973 - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
1976 return [
FlutterTextRange rangeWithNSRange:fml::RangeForCharacterAtIndex(self.text, currentIndex)];
2007 - (void)beginFloatingCursorAtPoint:(CGPoint)point {
2024 [
self.textInputDelegate flutterTextInputView:self
2025 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2026 withClient:_textInputClient
2027 withPosition:@{@"X" : @0, @"Y" : @0}];
2030 - (void)updateFloatingCursorAtPoint:(CGPoint)point {
2031 [
self.textInputDelegate flutterTextInputView:self
2032 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2033 withClient:_textInputClient
2035 @"X" : @(point.x - _floatingCursorOffset.x),
2036 @"Y" : @(point.y - _floatingCursorOffset.y)
2040 - (void)endFloatingCursor {
2042 [
self.textInputDelegate flutterTextInputView:self
2043 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2044 withClient:_textInputClient
2045 withPosition:@{@"X" : @0, @"Y" : @0}];
2048 #pragma mark - UIKeyInput Overrides
2050 - (void)updateEditingState {
2055 NSInteger composingBase = -1;
2056 NSInteger composingExtent = -1;
2061 NSDictionary* state = @{
2062 @"selectionBase" : @(selectionBase),
2063 @"selectionExtent" : @(selectionExtent),
2065 @"selectionIsDirectional" : @(
false),
2066 @"composingBase" : @(composingBase),
2067 @"composingExtent" : @(composingExtent),
2068 @"text" : [NSString stringWithString:self.text],
2071 if (_textInputClient == 0 && _autofillId != nil) {
2072 [
self.textInputDelegate flutterTextInputView:self
2073 updateEditingClient:_textInputClient
2075 withTag:_autofillId];
2077 [
self.textInputDelegate flutterTextInputView:self
2078 updateEditingClient:_textInputClient
2083 - (void)updateEditingStateWithDelta:(
flutter::TextEditingDelta)delta {
2088 NSInteger composingBase = -1;
2089 NSInteger composingExtent = -1;
2095 NSDictionary* deltaToFramework = @{
2096 @"oldText" : @(delta.old_text().c_str()),
2097 @"deltaText" : @(delta.delta_text().c_str()),
2098 @"deltaStart" : @(delta.delta_start()),
2099 @"deltaEnd" : @(delta.delta_end()),
2100 @"selectionBase" : @(selectionBase),
2101 @"selectionExtent" : @(selectionExtent),
2103 @"selectionIsDirectional" : @(
false),
2104 @"composingBase" : @(composingBase),
2105 @"composingExtent" : @(composingExtent),
2108 [_pendingDeltas addObject:deltaToFramework];
2110 if (_pendingDeltas.count == 1) {
2112 dispatch_async(dispatch_get_main_queue(), ^{
2114 if (strongSelf && strongSelf.pendingDeltas.count > 0) {
2115 NSDictionary* deltas = @{
2116 @"deltas" : strongSelf.pendingDeltas,
2119 [strongSelf.textInputDelegate flutterTextInputView:strongSelf
2120 updateEditingClient:strongSelf->_textInputClient
2122 [strongSelf.pendingDeltas removeAllObjects];
2129 return self.text.length > 0;
2132 - (void)insertText:(NSString*)text {
2133 if (
self.temporarilyDeletedComposedCharacter.length > 0 && text.length == 1 && !text.UTF8String &&
2134 [text characterAtIndex:0] == [
self.temporarilyDeletedComposedCharacter characterAtIndex:0]) {
2138 text =
self.temporarilyDeletedComposedCharacter;
2139 self.temporarilyDeletedComposedCharacter = nil;
2142 NSMutableArray<FlutterTextSelectionRect*>* copiedRects =
2143 [[NSMutableArray alloc] initWithCapacity:[_selectionRects count]];
2145 @"Expected a FlutterTextPosition for position (got %@).",
2148 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2149 NSUInteger rectPosition = _selectionRects[i].position;
2150 if (rectPosition == insertPosition) {
2151 for (NSUInteger j = 0; j <= text.length; j++) {
2158 if (rectPosition > insertPosition) {
2159 rectPosition = rectPosition + text.length;
2168 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2169 [
self resetScribbleInteractionStatusIfEnding];
2170 self.selectionRects = copiedRects;
2172 [
self replaceRange:_selectedTextRange withText:text];
2175 - (UITextPlaceholder*)insertTextPlaceholderWithSize:(CGSize)size API_AVAILABLE(ios(13.0)) {
2176 [
self.textInputDelegate flutterTextInputView:self
2177 insertTextPlaceholderWithSize:size
2178 withClient:_textInputClient];
2183 - (void)removeTextPlaceholder:(UITextPlaceholder*)textPlaceholder API_AVAILABLE(ios(13.0)) {
2185 [
self.textInputDelegate flutterTextInputView:self removeTextPlaceholder:_textInputClient];
2188 - (void)deleteBackward {
2190 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2191 [
self resetScribbleInteractionStatusIfEnding];
2208 if (oldRange.location > 0) {
2209 NSRange newRange = NSMakeRange(oldRange.location - 1, 1);
2213 NSRange charRange = fml::RangeForCharacterAtIndex(
self.text, oldRange.location - 1);
2214 if (
IsEmoji(
self.text, charRange)) {
2215 newRange = NSMakeRange(charRange.location, oldRange.location - charRange.location);
2227 NSString* deletedText = [
self.text substringWithRange:_selectedTextRange.range];
2228 NSRange deleteFirstCharacterRange = fml::RangeForCharacterAtIndex(deletedText, 0);
2229 self.temporarilyDeletedComposedCharacter =
2230 [deletedText substringWithRange:deleteFirstCharacterRange];
2232 [
self replaceRange:_selectedTextRange withText:@""];
2236 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(
id)target {
2237 UIAccessibilityPostNotification(notification, target);
2240 - (void)accessibilityElementDidBecomeFocused {
2241 if ([
self accessibilityElementIsFocused]) {
2245 FML_DCHECK(_backingTextInputAccessibilityObject);
2246 [
self postAccessibilityNotification:UIAccessibilityScreenChangedNotification
2247 target:_backingTextInputAccessibilityObject];
2251 - (BOOL)accessibilityElementsHidden {
2252 return !_accessibilityEnabled;
2261 #pragma mark - Key Events Handling
2262 - (void)pressesBegan:(NSSet<UIPress*>*)presses
2263 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2264 [_textInputPlugin.viewController pressesBegan:presses withEvent:event];
2267 - (void)pressesChanged:(NSSet<UIPress*>*)presses
2268 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2269 [_textInputPlugin.viewController pressesChanged:presses withEvent:event];
2272 - (void)pressesEnded:(NSSet<UIPress*>*)presses
2273 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2274 [_textInputPlugin.viewController pressesEnded:presses withEvent:event];
2277 - (void)pressesCancelled:(NSSet<UIPress*>*)presses
2278 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2279 [_textInputPlugin.viewController pressesCancelled:presses withEvent:event];
2308 - (BOOL)accessibilityElementsHidden {
2315 - (void)enableActiveViewAccessibility;
2332 - (void)enableActiveViewAccessibility {
2333 [
self.target enableActiveViewAccessibility];
2340 @property(nonatomic, readonly)
2341 NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
2346 @property(nonatomic, strong) UIView* keyboardViewContainer;
2347 @property(nonatomic, strong) UIView* keyboardView;
2348 @property(nonatomic, strong) UIView* cachedFirstResponder;
2349 @property(nonatomic, assign) CGRect keyboardRect;
2350 @property(nonatomic, assign) CGFloat previousPointerYPosition;
2351 @property(nonatomic, assign) CGFloat pointerYVelocity;
2355 NSTimer* _enableFlutterTextInputViewAccessibilityTimer;
2359 self = [
super init];
2362 _textInputDelegate = textInputDelegate;
2363 _autofillContext = [[NSMutableDictionary alloc] init];
2365 _scribbleElements = [[NSMutableDictionary alloc] init];
2366 _keyboardViewContainer = [[UIView alloc] init];
2368 [[NSNotificationCenter defaultCenter] addObserver:self
2369 selector:@selector(handleKeyboardWillShow:)
2370 name:UIKeyboardWillShowNotification
2377 - (void)handleKeyboardWillShow:(NSNotification*)notification {
2378 NSDictionary* keyboardInfo = [notification userInfo];
2379 NSValue* keyboardFrameEnd = [keyboardInfo valueForKey:UIKeyboardFrameEndUserInfoKey];
2380 _keyboardRect = [keyboardFrameEnd CGRectValue];
2384 [
self hideTextInput];
2387 - (void)removeEnableFlutterTextInputViewAccessibilityTimer {
2388 if (_enableFlutterTextInputViewAccessibilityTimer) {
2389 [_enableFlutterTextInputViewAccessibilityTimer invalidate];
2390 _enableFlutterTextInputViewAccessibilityTimer = nil;
2394 - (UIView<UITextInput>*)textInputView {
2399 NSString* method = call.
method;
2402 [
self showTextInput];
2404 }
else if ([method isEqualToString:
kHideMethod]) {
2405 [
self hideTextInput];
2408 [
self setTextInputClient:[args[0] intValue] withConfiguration:args[1]];
2412 [
self setPlatformViewTextInputClient];
2415 [
self setTextInputEditingState:args];
2418 [
self clearTextInputClient];
2421 [
self setEditableSizeAndTransform:args];
2424 [
self updateMarkedRect:args];
2427 [
self triggerAutofillSave:[args boolValue]];
2433 [
self setSelectionRects:args];
2436 [
self setSelectionRects:args];
2439 [
self startLiveTextInput];
2442 [
self updateConfig:args];
2445 CGFloat pointerY = (CGFloat)[args[
@"pointerY"] doubleValue];
2446 [
self handlePointerMove:pointerY];
2449 CGFloat pointerY = (CGFloat)[args[
@"pointerY"] doubleValue];
2450 [
self handlePointerUp:pointerY];
2457 - (void)handlePointerUp:(CGFloat)pointerY {
2458 if (_keyboardView.superview != nil) {
2461 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2462 CGFloat screenHeight = screen.bounds.size.height;
2463 CGFloat keyboardHeight = _keyboardRect.size.height;
2465 BOOL shouldDismissKeyboardBasedOnVelocity = _pointerYVelocity < 0;
2466 [UIView animateWithDuration:kKeyboardAnimationTimeToCompleteion
2468 double keyboardDestination =
2469 shouldDismissKeyboardBasedOnVelocity ? screenHeight : screenHeight - keyboardHeight;
2470 _keyboardViewContainer.frame = CGRectMake(
2471 0, keyboardDestination, _viewController.flutterScreenIfViewLoaded.bounds.size.width,
2472 _keyboardViewContainer.frame.size.height);
2474 completion:^(BOOL finished) {
2475 if (shouldDismissKeyboardBasedOnVelocity) {
2476 [
self.textInputDelegate flutterTextInputView:self.activeView
2477 didResignFirstResponderWithTextInputClient:self.activeView.textInputClient];
2478 [
self dismissKeyboardScreenshot];
2480 [
self showKeyboardAndRemoveScreenshot];
2486 - (void)dismissKeyboardScreenshot {
2487 for (UIView* subView in _keyboardViewContainer.subviews) {
2488 [subView removeFromSuperview];
2492 - (void)showKeyboardAndRemoveScreenshot {
2493 [UIView setAnimationsEnabled:NO];
2494 [_cachedFirstResponder becomeFirstResponder];
2498 dispatch_get_main_queue(), ^{
2499 [UIView setAnimationsEnabled:YES];
2500 [
self dismissKeyboardScreenshot];
2504 - (void)handlePointerMove:(CGFloat)pointerY {
2506 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2507 CGFloat screenHeight = screen.bounds.size.height;
2508 CGFloat keyboardHeight = _keyboardRect.size.height;
2509 if (screenHeight - keyboardHeight <= pointerY) {
2511 if (_keyboardView.superview == nil) {
2513 [
self takeKeyboardScreenshotAndDisplay];
2514 [
self hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate];
2516 [
self setKeyboardContainerHeight:pointerY];
2517 _pointerYVelocity = _previousPointerYPosition - pointerY;
2520 if (_keyboardView.superview != nil) {
2522 _keyboardViewContainer.frame = _keyboardRect;
2523 _pointerYVelocity = _previousPointerYPosition - pointerY;
2526 _previousPointerYPosition = pointerY;
2529 - (void)setKeyboardContainerHeight:(CGFloat)pointerY {
2530 CGRect frameRect = _keyboardRect;
2531 frameRect.origin.y = pointerY;
2532 _keyboardViewContainer.frame = frameRect;
2535 - (void)hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate {
2536 [UIView setAnimationsEnabled:NO];
2537 _cachedFirstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder;
2538 _activeView.preventCursorDismissWhenResignFirstResponder = YES;
2539 [_cachedFirstResponder resignFirstResponder];
2540 _activeView.preventCursorDismissWhenResignFirstResponder = NO;
2541 [UIView setAnimationsEnabled:YES];
2544 - (void)takeKeyboardScreenshotAndDisplay {
2546 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2547 UIView* keyboardSnap = [screen snapshotViewAfterScreenUpdates:YES];
2548 keyboardSnap = [keyboardSnap resizableSnapshotViewFromRect:_keyboardRect
2549 afterScreenUpdates:YES
2550 withCapInsets:UIEdgeInsetsZero];
2551 _keyboardView = keyboardSnap;
2552 [_keyboardViewContainer addSubview:_keyboardView];
2553 if (_keyboardViewContainer.superview == nil) {
2554 [UIApplication.sharedApplication.delegate.window.rootViewController.view
2555 addSubview:_keyboardViewContainer];
2557 _keyboardViewContainer.layer.zPosition = NSIntegerMax;
2558 _keyboardViewContainer.frame = _keyboardRect;
2561 - (BOOL)showEditMenu:(NSDictionary*)args API_AVAILABLE(ios(16.0)) {
2562 if (!
self.activeView.isFirstResponder) {
2565 NSDictionary<NSString*, NSNumber*>* encodedTargetRect = args[@"targetRect"];
2566 CGRect globalTargetRect = CGRectMake(
2567 [encodedTargetRect[
@"x"] doubleValue], [encodedTargetRect[
@"y"] doubleValue],
2568 [encodedTargetRect[
@"width"] doubleValue], [encodedTargetRect[
@"height"] doubleValue]);
2569 CGRect localTargetRect = [
self.hostView convertRect:globalTargetRect toView:self.activeView];
2570 [
self.activeView showEditMenuWithTargetRect:localTargetRect];
2574 - (void)hideEditMenu {
2575 [
self.activeView hideEditMenu];
2578 - (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
2579 NSArray* transform = dictionary[@"transform"];
2580 [_activeView setEditableTransform:transform];
2581 const int leftIndex = 12;
2582 const int topIndex = 13;
2586 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue],
2587 [dictionary[
@"width"] intValue], [dictionary[
@"height"] intValue]);
2589 CGRectMake(0, 0, [dictionary[
@"width"] intValue], [dictionary[
@"height"] intValue]);
2590 _activeView.tintColor = [UIColor clearColor];
2595 if (@available(iOS 17, *)) {
2603 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue], 0, 0);
2608 - (void)updateMarkedRect:(NSDictionary*)dictionary {
2609 NSAssert(dictionary[
@"x"] != nil && dictionary[
@"y"] != nil && dictionary[
@"width"] != nil &&
2610 dictionary[
@"height"] != nil,
2611 @"Expected a dictionary representing a CGRect, got %@", dictionary);
2612 CGRect rect = CGRectMake([dictionary[
@"x"] doubleValue], [dictionary[
@"y"] doubleValue],
2613 [dictionary[
@"width"] doubleValue], [dictionary[
@"height"] doubleValue]);
2614 _activeView.markedRect = rect.size.width < 0 && rect.size.height < 0 ?
kInvalidFirstRect : rect;
2617 - (void)setSelectionRects:(NSArray*)encodedRects {
2618 NSMutableArray<FlutterTextSelectionRect*>* rectsAsRect =
2619 [[NSMutableArray alloc] initWithCapacity:[encodedRects count]];
2620 for (NSUInteger i = 0; i < [encodedRects count]; i++) {
2621 NSArray<NSNumber*>* encodedRect = encodedRects[i];
2623 selectionRectWithRect:CGRectMake([encodedRect[0] floatValue],
2624 [encodedRect[1] floatValue],
2625 [encodedRect[2] floatValue],
2626 [encodedRect[3] floatValue])
2627 position:[encodedRect[4] unsignedIntegerValue]
2628 writingDirection:[encodedRect[5] unsignedIntegerValue] == 1
2629 ? NSWritingDirectionLeftToRight
2630 : NSWritingDirectionRightToLeft]];
2636 _activeView.selectionRects = rectsAsRect;
2639 - (void)startLiveTextInput {
2640 if (@available(iOS 15.0, *)) {
2641 if (_activeView == nil || !_activeView.isFirstResponder) {
2644 [_activeView captureTextFromCamera:nil];
2648 - (void)showTextInput {
2649 _activeView.viewResponder = _viewResponder;
2650 [
self addToInputParentViewIfNeeded:_activeView];
2659 if (!_enableFlutterTextInputViewAccessibilityTimer) {
2660 _enableFlutterTextInputViewAccessibilityTimer =
2661 [NSTimer scheduledTimerWithTimeInterval:kUITextInputAccessibilityEnablingDelaySeconds
2663 selector:@selector(enableActiveViewAccessibility)
2667 [_activeView becomeFirstResponder];
2670 - (void)enableActiveViewAccessibility {
2671 if (_activeView.isFirstResponder) {
2672 _activeView.accessibilityEnabled = YES;
2674 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2677 - (void)hideTextInput {
2678 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2679 _activeView.accessibilityEnabled = NO;
2680 [_activeView resignFirstResponder];
2681 [_activeView removeFromSuperview];
2682 [_inputHider removeFromSuperview];
2685 - (void)triggerAutofillSave:(BOOL)saveEntries {
2686 [_activeView resignFirstResponder];
2691 [
self cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
2692 [_autofillContext removeAllObjects];
2693 [
self changeInputViewsAutofillVisibility:YES];
2695 [_autofillContext removeAllObjects];
2698 [
self cleanUpViewHierarchy:YES clearText:!saveEntries delayRemoval:NO];
2699 [
self addToInputParentViewIfNeeded:_activeView];
2702 - (void)setPlatformViewTextInputClient {
2706 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2707 _activeView.accessibilityEnabled = NO;
2708 [_activeView removeFromSuperview];
2709 [_inputHider removeFromSuperview];
2712 - (void)setTextInputClient:(
int)client withConfiguration:(NSDictionary*)configuration {
2713 [
self resetAllClientIds];
2716 [
self changeInputViewsAutofillVisibility:NO];
2720 case kFlutterAutofillTypeNone:
2721 self.activeView = [
self createInputViewWith:configuration];
2723 case kFlutterAutofillTypeRegular:
2726 self.activeView = [
self updateAndShowAutofillViews:nil
2727 focusedField:configuration
2728 isPasswordRelated:NO];
2730 case kFlutterAutofillTypePassword:
2731 self.activeView = [
self updateAndShowAutofillViews:configuration[kAssociatedAutofillFields]
2732 focusedField:configuration
2733 isPasswordRelated:YES];
2736 [_activeView setTextInputClient:client];
2737 [_activeView reloadInputViews];
2749 [
self cleanUpViewHierarchy:NO clearText:YES delayRemoval:YES];
2760 [_autofillContext removeObjectForKey:autofillId];
2763 [newView configureWithDictionary:configuration];
2764 [
self addToInputParentViewIfNeeded:newView];
2768 if (autofillId &&
AutofillTypeOf(field) == kFlutterAutofillTypeNone) {
2769 [_autofillContext removeObjectForKey:autofillId];
2776 focusedField:(NSDictionary*)focusedField
2777 isPasswordRelated:(BOOL)isPassword {
2780 NSAssert(focusedId,
@"autofillId must not be null for the focused field: %@", focusedField);
2785 focused = [
self getOrCreateAutofillableView:focusedField isPasswordAutofill:isPassword];
2786 [_autofillContext removeObjectForKey:focusedId];
2789 for (NSDictionary* field in fields) {
2791 NSAssert(autofillId,
@"autofillId must not be null for field: %@", field);
2793 BOOL hasHints =
AutofillTypeOf(field) != kFlutterAutofillTypeNone;
2794 BOOL isFocused = [focusedId isEqualToString:autofillId];
2797 focused = [
self getOrCreateAutofillableView:field isPasswordAutofill:isPassword];
2802 _autofillContext[autofillId] = isFocused ? focused
2803 : [
self getOrCreateAutofillableView:field
2804 isPasswordAutofill:isPassword];
2807 [_autofillContext removeObjectForKey:autofillId];
2811 NSAssert(focused,
@"The current focused input view must not be nil.");
2821 isPasswordAutofill:(BOOL)needsPasswordAutofill {
2827 inputView = [inputView initWithOwner:self];
2828 [
self addToInputParentViewIfNeeded:inputView];
2831 [inputView configureWithDictionary:field];
2836 - (UIView*)hostView {
2837 UIView* host = _viewController.view;
2838 NSAssert(host !=
nullptr,
2839 @"The application must have a host view since the keyboard client "
2840 @"must be part of the responder chain to function. The host view controller is %@",
2846 - (NSArray<UIView*>*)textInputViews {
2847 return _inputHider.subviews;
2860 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
2861 clearText:(BOOL)clearText
2862 delayRemoval:(BOOL)delayRemoval {
2863 for (UIView* view in
self.textInputViews) {
2865 (includeActiveView || view != _activeView)) {
2867 if (_autofillContext[inputView.autofillId] != view) {
2869 [inputView replaceRangeLocal:NSMakeRange(0, inputView.text.length) withText:@""];
2872 [inputView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:0.1];
2874 [inputView removeFromSuperview];
2883 - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
2884 for (UIView* view in
self.textInputViews) {
2887 inputView.isVisibleToAutofill = newVisibility;
2899 - (void)resetAllClientIds {
2900 for (UIView* view in
self.textInputViews) {
2903 [inputView setTextInputClient:0];
2909 if (![inputView isDescendantOfView:_inputHider]) {
2910 [_inputHider addSubview:inputView];
2913 if (_viewController.view == nil) {
2919 UIView* parentView =
self.hostView;
2920 if (_inputHider.superview != parentView) {
2921 [parentView addSubview:_inputHider];
2925 - (void)setTextInputEditingState:(NSDictionary*)state {
2926 [_activeView setTextInputState:state];
2929 - (void)clearTextInputClient {
2930 [_activeView setTextInputClient:0];
2931 _activeView.frame = CGRectZero;
2934 - (void)updateConfig:(NSDictionary*)dictionary {
2935 BOOL isSecureTextEntry = [dictionary[kSecureTextEntry] boolValue];
2936 for (UIView* view in
self.textInputViews) {
2943 if (inputView.isSecureTextEntry != isSecureTextEntry) {
2944 inputView.secureTextEntry = isSecureTextEntry;
2945 [inputView reloadInputViews];
2951 #pragma mark UIIndirectScribbleInteractionDelegate
2953 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2954 isElementFocused:(UIScribbleElementIdentifier)elementIdentifier
2955 API_AVAILABLE(ios(14.0)) {
2956 return _activeView.scribbleFocusStatus == FlutterScribbleFocusStatusFocused;
2959 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2960 focusElementIfNeeded:(UIScribbleElementIdentifier)elementIdentifier
2961 referencePoint:(CGPoint)focusReferencePoint
2962 completion:(
void (^)(UIResponder<UITextInput>* focusedInput))completion
2963 API_AVAILABLE(ios(14.0)) {
2964 _activeView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
2965 [_indirectScribbleDelegate flutterTextInputPlugin:self
2966 focusElement:elementIdentifier
2967 atPoint:focusReferencePoint
2968 result:^(id _Nullable result) {
2969 _activeView.scribbleFocusStatus =
2970 FlutterScribbleFocusStatusFocused;
2971 completion(_activeView);
2975 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2976 shouldDelayFocusForElement:(UIScribbleElementIdentifier)elementIdentifier
2977 API_AVAILABLE(ios(14.0)) {
2981 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2982 willBeginWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
2983 API_AVAILABLE(ios(14.0)) {
2986 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2987 didFinishWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
2988 API_AVAILABLE(ios(14.0)) {
2991 - (CGRect)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2992 frameForElement:(UIScribbleElementIdentifier)elementIdentifier
2993 API_AVAILABLE(ios(14.0)) {
2994 NSValue* elementValue = [_scribbleElements objectForKey:elementIdentifier];
2995 if (elementValue == nil) {
2998 return [elementValue CGRectValue];
3001 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3002 requestElementsInRect:(CGRect)rect
3004 (
void (^)(NSArray<UIScribbleElementIdentifier>* elements))completion
3005 API_AVAILABLE(ios(14.0)) {
3006 [_indirectScribbleDelegate
3007 flutterTextInputPlugin:self
3008 requestElementsInRect:rect
3009 result:^(id _Nullable result) {
3010 NSMutableArray<UIScribbleElementIdentifier>* elements =
3011 [[NSMutableArray alloc] init];
3012 if ([result isKindOfClass:[NSArray class]]) {
3013 for (NSArray* elementArray in result) {
3014 [elements addObject:elementArray[0]];
3017 valueWithCGRect:CGRectMake(
3018 [elementArray[1] floatValue],
3019 [elementArray[2] floatValue],
3020 [elementArray[3] floatValue],
3021 [elementArray[4] floatValue])]
3022 forKey:elementArray[0]];
3025 completion(elements);
3029 #pragma mark - Methods related to Scribble support
3033 if (@available(iOS 14.0, *)) {
3035 if (parentView != nil) {
3036 UIIndirectScribbleInteraction* scribbleInteraction = [[UIIndirectScribbleInteraction alloc]
3037 initWithDelegate:(id<UIIndirectScribbleInteractionDelegate>)self];
3038 [parentView addInteraction:scribbleInteraction];
3045 - (void)resetViewResponder {
3046 _viewResponder = nil;
3050 #pragma mark FlutterKeySecondaryResponder
3065 - (id)flutterFirstResponder {
3066 if (
self.isFirstResponder) {
3069 for (UIView* subView in
self.subviews) {
3070 UIView* firstResponder = subView.flutterFirstResponder;
3071 if (firstResponder) {
3072 return firstResponder;