Skip to content

Commit

Permalink
Implement UIAsyncTextInput methods to request word rects for and appl…
Browse files Browse the repository at this point in the history
…y autocorrections

https://bugs.webkit.org/show_bug.cgi?id=263388
rdar://117219847

Reviewed by Tim Horton and Abrar Rahman Protyasha.

Adopt several new `UIAsyncTextInput` (and adjacent) methods and classes:

```
UIKeyEventContext
-requestTextRectsForString:withCompletionHandler:
-replaceText:withText:options:withCompletionHandler:
-deferEventHandlingToSystemWithContext:
```

See below for more details.

* Source/WTF/wtf/PlatformHave.h:

Add a compile-time flag to guard the availability of these new APIs.

* Source/WebKit/Platform/spi/ios/UIKitSPI.h:

Import some of these new private headers.

* Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm:
(+[WKUITextSelectionRect selectionRectWithCGRect:]):
(-[WKUITextSelectionRect initWithCGRect:]):
(-[WKUITextSelectionRect rect]):

Add a custom `UITextSelectionRect` subclass that we can initialize with a `CGRect`.

(-[WKContentView _ensureBinaryCompatibilityWithAsyncInteractionsIfNeeded]):

To make debugging and bincompat sanity-checking a bit more convenient, dump all optional methods on
`UIAsyncTextInput` that are unimplemented on the content view.

(-[WKContentView requestAutocorrectionRectsForString:withCompletionHandler:]):
(-[WKContentView _internalRequestTextRectsForString:completion:]):

Refactor these two legacy `UIWKInteractionViewProtocol` methods to use the same internal helper
method as the new `UIAsyncTextInput` methods. Note that these internal helper methods don't use any
of the legacy `UIWK*` classes; this will make it easier to cleanly excise these deprecated codepaths
after the minimum supported iOS version has system support for `UIAsyncTextInput`.

(-[WKContentView applyAutocorrection:toString:isCandidate:withCompletionHandler:]):
(-[WKContentView _internalReplaceText:withText:isCandidate:completion:]):
(-[WKContentView _interpretKeyEvent:isCharEvent:]):

Set up a key event context object when deferring key event handling to the system; this allows
only `keypress` events to trigger text editing, while still allowing `keydown` events to trigger
other actions, like menu commands.

(-[WKContentView replaceText:withText:options:withCompletionHandler:]):
(-[WKContentView requestTextRectsForString:withCompletionHandler:]):

Canonical link: https://commits.webkit.org/269593@main
  • Loading branch information
whsieh committed Oct 20, 2023
1 parent 7528a0a commit 3099709
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 20 deletions.
4 changes: 4 additions & 0 deletions Source/WTF/wtf/PlatformHave.h
Original file line number Diff line number Diff line change
Expand Up @@ -1661,6 +1661,10 @@
#define HAVE_UI_ASYNC_DRAG_INTERACTION 1
#endif

#if __has_include(<UIKit/UIAsyncTextInput.h>) && __has_include(<UIKit/UIKeyEventContext.h>)
#define HAVE_UI_ASYNC_TEXT_INPUT 1
#endif

#if !PLATFORM(WATCHOS) && __has_include(<UIKit/_UIContextMenuAsyncConfiguration.h>)
#define HAVE_UI_CONTEXT_MENU_ASYNC_CONFIGURATION 1
#endif
5 changes: 5 additions & 0 deletions Source/WebKit/Platform/spi/ios/UIKitSPI.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@
#import <UIKit/_UITextDragCaretView.h>
#endif

#if HAVE(UI_ASYNC_TEXT_INPUT)
#import <UIKit/UIAsyncTextInput.h>
#import <UIKit/UIKeyEventContext.h>
#endif

#if HAVE(UI_ASYNC_DRAG_INTERACTION)
#import <UIKit/UIDragInteraction_AsyncSupport.h>
#import <UIKit/_UIAsyncDragInteraction.h>
Expand Down
121 changes: 101 additions & 20 deletions Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
#import <wtf/CallbackAggregator.h>
#import <wtf/Scope.h>
#import <wtf/SetForScope.h>
#import <wtf/SystemFree.h>
#import <wtf/WeakObjCPtr.h>
#import <wtf/cocoa/NSURLExtras.h>
#import <wtf/cocoa/RuntimeApplicationChecksCocoa.h>
Expand Down Expand Up @@ -195,11 +196,43 @@
SOFT_LINK_FRAMEWORK(UIKit)
SOFT_LINK_CLASS_OPTIONAL(UIKit, _UIAsyncDragInteraction)
SOFT_LINK_CLASS_OPTIONAL(UIKit, _UIContextMenuAsyncConfiguration)
SOFT_LINK_CLASS_OPTIONAL(UIKit, UIKeyEventContext)

#if HAVE(AUTOCORRECTION_ENHANCEMENTS)
#define UIWKDocumentRequestAutocorrectedRanges (1 << 7)
#endif

#if HAVE(UI_ASYNC_TEXT_INPUT)

@interface WKUITextSelectionRect : UITextSelectionRect
+ (instancetype)selectionRectWithCGRect:(CGRect)rect;
@end

@implementation WKUITextSelectionRect {
CGRect _selectionRect;
}

+ (instancetype)selectionRectWithCGRect:(CGRect)rect
{
return [[[WKUITextSelectionRect alloc] initWithCGRect:rect] autorelease];
}

- (instancetype)initWithCGRect:(CGRect)rect
{
if (self = [super init])
_selectionRect = rect;
return self;
}

- (CGRect)rect
{
return _selectionRect;
}

@end

#endif // HAVE(UI_ASYNC_TEXT_INPUT)

#if HAVE(LINK_PREVIEW) && USE(UICONTEXTMENU)
static NSString * const webkitShowLinkPreviewsPreferenceKey = @"WebKitShowLinkPreviews";
#endif
Expand Down Expand Up @@ -925,9 +958,19 @@ - (void)_ensureBinaryCompatibilityWithAsyncInteractionsIfNeeded
static std::once_flag onceFlag;
std::call_once(onceFlag, ^{
#if HAVE(UI_ASYNC_TEXT_INPUT)
if (_page->preferences().useAsyncUIKitInteractions())
if (_page->preferences().useAsyncUIKitInteractions()) {
class_addProtocol(self.class, @protocol(UIAsyncTextInput));
#endif
unsigned methodCount = 0;
using MethodDescriptionList = objc_method_description[];
auto methods = std::unique_ptr<MethodDescriptionList, WTF::SystemFree<MethodDescriptionList>> {
protocol_copyMethodDescriptionList(@protocol(UIAsyncTextInput), NO, YES, &methodCount)
};
for (unsigned i = 0; i < methodCount; i++) {
if (![self.class instancesRespondToSelector:methods[i].name])
RELEASE_LOG_ERROR(TextInteraction, "Warning: -[UIAsyncTextInput %s] is unimplemented", sel_getName(methods[i].name));
}
}
#endif // HAVE(UI_ASYNC_TEXT_INPUT)
#if USE(UICONTEXTMENU)
if (!self._shouldUseUIContextMenuAsyncConfiguration) {
// Only fall back to the legacy asynchronous delegate method when UI_CONTEXT_MENU_ASYNC_CONFIGURATION
Expand Down Expand Up @@ -4822,12 +4865,17 @@ - (void)requestAutocorrectionRectsForString:(NSString *)input withCompletionHand
return;
}

if (!input || ![input length]) {
completionHandler(nil);
return;
}
[self _internalRequestTextRectsForString:input completion:[view = retainPtr(self), completionHandler = makeBlockPtr(completionHandler)](auto& rects) {
completionHandler(!rects.isEmpty() ? [WKAutocorrectionRects autocorrectionRectsWithFirstCGRect:rects.first() lastCGRect:rects.last()] : nil);
}];
}

- (void)_internalRequestTextRectsForString:(NSString *)input completion:(Function<void(const Vector<WebCore::FloatRect>&)>&&)completion
{
if (!input.length)
return completion({ });

_page->requestAutocorrectionData(input, [view = retainPtr(self), completion = makeBlockPtr(completionHandler)](auto data) {
_page->requestAutocorrectionData(input, [view = retainPtr(self), completion = WTFMove(completion)](const WebKit::WebAutocorrectionData& data) mutable {
CGRect firstRect;
CGRect lastRect;
auto& rects = data.textRects;
Expand All @@ -4843,7 +4891,7 @@ - (void)requestAutocorrectionRectsForString:(NSString *)input withCompletionHand
view->_autocorrectionData.textFirstRect = firstRect;
view->_autocorrectionData.textLastRect = lastRect;

completion(!rects.isEmpty() ? [WKAutocorrectionRects autocorrectionRectsWithFirstCGRect:firstRect lastCGRect:lastRect] : nil);
completion(rects);
});
}

Expand Down Expand Up @@ -5230,10 +5278,17 @@ - (void)requestDictationContext:(void (^)(NSString *selectedText, NSString *befo

// The completion handler should pass the rect of the correction text after replacing the input text, or nil if the replacement could not be performed.
- (void)applyAutocorrection:(NSString *)correction toString:(NSString *)input isCandidate:(BOOL)isCandidate withCompletionHandler:(void (^)(UIWKAutocorrectionRects *rectsForCorrection))completionHandler
{
[self _internalReplaceText:input withText:correction isCandidate:isCandidate completion:[completionHandler = makeBlockPtr(completionHandler), view = retainPtr(self)](bool wasApplied) {
completionHandler(wasApplied ? [WKAutocorrectionRects autocorrectionRectsWithFirstCGRect:view->_autocorrectionData.textFirstRect lastCGRect:view->_autocorrectionData.textLastRect] : nil);
}];
}

- (void)_internalReplaceText:(NSString *)input withText:(NSString *)correction isCandidate:(BOOL)isCandidate completion:(Function<void(bool)>&&)completionHandler
{
if ([self _disableAutomaticKeyboardUI]) {
if (completionHandler)
completionHandler(nil);
completionHandler(false);
return;
}

Expand All @@ -5242,13 +5297,13 @@ - (void)applyAutocorrection:(NSString *)correction toString:(NSString *)input is

if (useSyncRequest) {
if (completionHandler)
completionHandler(_page->applyAutocorrection(correction, input, isCandidate) ? [WKAutocorrectionRects autocorrectionRectsWithFirstCGRect:_autocorrectionData.textFirstRect lastCGRect:_autocorrectionData.textLastRect] : nil);
completionHandler(_page->applyAutocorrection(correction, input, isCandidate));
return;
}

_page->applyAutocorrection(correction, input, isCandidate, [view = retainPtr(self), completion = makeBlockPtr(completionHandler)](auto& string) {
if (completion)
completion(!string.isNull() ? [WKAutocorrectionRects autocorrectionRectsWithFirstCGRect:view->_autocorrectionData.textFirstRect lastCGRect:view->_autocorrectionData.textLastRect] : nil);
_page->applyAutocorrection(correction, input, isCandidate, [view = retainPtr(self), completionHandler = WTFMove(completionHandler)](auto& string) {
if (completionHandler)
completionHandler(!string.isNull());
});
}

Expand Down Expand Up @@ -6690,13 +6745,17 @@ - (void)_didHandleKeyEvent:(::WebEvent *)event eventWasHandled:(BOOL)eventWasHan

- (BOOL)_interpretKeyEvent:(::WebEvent *)event isCharEvent:(BOOL)isCharEvent
{
if ([_keyboardScrollingAnimator beginWithEvent:event] || [_keyboardScrollingAnimator scrollTriggeringKeyIsPressed])
return YES;

#if HAVE(UI_ASYNC_TEXT_INPUT)
if (auto systemDelegate = retainPtr(_asyncSystemInputDelegate)) {
auto originalKeyEvent = retainPtr(event.originalUIKeyEvent);
ASSERT(originalKeyEvent);
return [systemDelegate deferHandlingToSystemForKeyEvent:originalKeyEvent.get()];
auto context = adoptNS([allocUIKeyEventContextInstance() initWithKeyEvent:event.originalUIKeyEvent]);
[context setDocumentIsEditable:_page->editorState().isContentEditable];
[context setShouldInsertChar:isCharEvent];
return [systemDelegate deferEventHandlingToSystemWithContext:context.get()];
}
#endif
#endif // HAVE(UI_ASYNC_TEXT_INPUT)

if (event.keyboardFlags & WebEventKeyboardInputModifierFlagsChanged)
return NO;
Expand All @@ -6706,9 +6765,6 @@ - (BOOL)_interpretKeyEvent:(::WebEvent *)event isCharEvent:(BOOL)isCharEvent
if (!contentEditable && event.isTabKey)
return NO;

if ([_keyboardScrollingAnimator beginWithEvent:event] || [_keyboardScrollingAnimator scrollTriggeringKeyIsPressed])
return YES;

UIKeyboardImpl *keyboard = [UIKeyboardImpl sharedInstance];

if (!isCharEvent && [keyboard handleKeyTextCommandForCurrentEvent])
Expand Down Expand Up @@ -11933,6 +11989,31 @@ - (void)handleAsyncKeyEvent:(UIKeyEvent *)event withCompletionHandler:(void(^)(U
}];
}

- (void)replaceText:(NSString *)originalText withText:(NSString *)replacementText options:(UITextReplacementOptions)options withCompletionHandler:(void (^)(NSArray<UITextSelectionRect *> *rects))completionHandler
{
[self _internalReplaceText:originalText withText:replacementText isCandidate:options & UITextReplacementOptionsAddUnderline completion:[view = retainPtr(self), completionHandler = makeBlockPtr(completionHandler)](bool wasReplaced) {
if (!wasReplaced)
return completionHandler(@[ ]);

auto& data = view->_autocorrectionData;
RetainPtr firstSelectionRect = [WKUITextSelectionRect selectionRectWithCGRect:data.textFirstRect];
if (CGRectEqualToRect(data.textFirstRect, data.textLastRect))
return completionHandler(@[ firstSelectionRect.get() ]);

completionHandler(@[ firstSelectionRect.get(), [WKUITextSelectionRect selectionRectWithCGRect:data.textLastRect] ]);
}];
}

- (void)requestTextRectsForString:(NSString *)input withCompletionHandler:(void (^)(NSArray<UITextSelectionRect *> *rects))completionHandler
{
[self _internalRequestTextRectsForString:input completion:[view = retainPtr(self), completionHandler = makeBlockPtr(completionHandler)](auto& rects) mutable {
auto selectionRects = createNSArray(rects, [](auto& rect) {
return [WKUITextSelectionRect selectionRectWithCGRect:rect];
});
completionHandler(selectionRects.get());
}];
}

- (UIView *)textInputView
{
return self;
Expand Down

0 comments on commit 3099709

Please sign in to comment.