[iOS] [UIWKDocumentContext] Add the ability to request all marked text rects
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sun, 17 Nov 2019 00:09:37 +0000 (00:09 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sun, 17 Nov 2019 00:09:37 +0000 (00:09 +0000)
https://bugs.webkit.org/show_bug.cgi?id=204278
<rdar://problem/56527803>

Reviewed by Tim Horton.

Source/WebCore:

Mark a constructor as WEBCORE_EXPORT. See WebKit ChangeLog for more details.

* editing/TextIterator.h:

Source/WebKit:

Add support for a new UIWKDocumentRequest option to request character rects for each character in the marked
text range. Unless UIWKDocumentRequestRects is additionally specified (in which case we'll return rects for
every single character in the editable root anyways), this option will ensure that all characters in the
composition range (which should match -[UIWKDocumentContext markedTextRange]) will have known character rects
that can be retrieved using -[UIWKDocumentContext enumerateLayoutRects:] (or one of the other helper methods
that utilize -enumerateLayoutRects:).

Test: WebKit.DocumentEditingContextWithMarkedText

* Platform/spi/ios/UIKitSPI.h:

Add a staging declaration for the new option.

* Shared/DocumentEditingContext.h:
* UIProcess/ios/WKContentViewInteraction.mm:
(toWebDocumentRequestOptions):
* WebProcess/WebPage/ios/WebPageIOS.mm:
(WebKit::WebPage::requestDocumentEditingContext):

Refactor logic that uses CharacterIterator to find rects for each character range into a lambda function, and
use this lambda function to handle UIWKDocumentRequestRects and the new UIWKDocumentRequestMarkedTextRects. The
marked text range is relative to the start of the context before the selection.

Tools:

Add an API test to exercise the new request option.

* TestWebKitAPI/Tests/WebKitCocoa/DocumentEditingContext.mm:
(-[UIWKDocumentContext markedTextRects]):
* TestWebKitAPI/ios/UIKitSPI.h:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@252528 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebCore/ChangeLog
Source/WebCore/editing/TextIterator.h
Source/WebKit/ChangeLog
Source/WebKit/Platform/spi/ios/UIKitSPI.h
Source/WebKit/Shared/DocumentEditingContext.h
Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm
Tools/ChangeLog
Tools/TestWebKitAPI/Tests/WebKitCocoa/DocumentEditingContext.mm
Tools/TestWebKitAPI/ios/UIKitSPI.h

index 6b24d68..31d2abb 100644 (file)
@@ -1,3 +1,15 @@
+2019-11-16  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] [UIWKDocumentContext] Add the ability to request all marked text rects
+        https://bugs.webkit.org/show_bug.cgi?id=204278
+        <rdar://problem/56527803>
+
+        Reviewed by Tim Horton.
+
+        Mark a constructor as WEBCORE_EXPORT. See WebKit ChangeLog for more details.
+
+        * editing/TextIterator.h:
+
 2019-11-16  Philippe Normand  <pnormand@igalia.com>
 
         Unreviewed, rolling out r252526.
index 5ae811a..10304af 100644 (file)
@@ -248,7 +248,7 @@ private:
 // character at a time, or faster, as needed. Useful for searching.
 class CharacterIterator {
 public:
-    explicit CharacterIterator(const Range&, TextIteratorBehavior = TextIteratorDefaultBehavior);
+    WEBCORE_EXPORT explicit CharacterIterator(const Range&, TextIteratorBehavior = TextIteratorDefaultBehavior);
     WEBCORE_EXPORT explicit CharacterIterator(Position start, Position end, TextIteratorBehavior = TextIteratorDefaultBehavior);
     
     bool atEnd() const { return m_underlyingIterator.atEnd(); }
index 1d87294..e5eba30 100644 (file)
@@ -1,3 +1,34 @@
+2019-11-16  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] [UIWKDocumentContext] Add the ability to request all marked text rects
+        https://bugs.webkit.org/show_bug.cgi?id=204278
+        <rdar://problem/56527803>
+
+        Reviewed by Tim Horton.
+
+        Add support for a new UIWKDocumentRequest option to request character rects for each character in the marked
+        text range. Unless UIWKDocumentRequestRects is additionally specified (in which case we'll return rects for
+        every single character in the editable root anyways), this option will ensure that all characters in the
+        composition range (which should match -[UIWKDocumentContext markedTextRange]) will have known character rects
+        that can be retrieved using -[UIWKDocumentContext enumerateLayoutRects:] (or one of the other helper methods
+        that utilize -enumerateLayoutRects:).
+
+        Test: WebKit.DocumentEditingContextWithMarkedText
+
+        * Platform/spi/ios/UIKitSPI.h:
+
+        Add a staging declaration for the new option.
+
+        * Shared/DocumentEditingContext.h:
+        * UIProcess/ios/WKContentViewInteraction.mm:
+        (toWebDocumentRequestOptions):
+        * WebProcess/WebPage/ios/WebPageIOS.mm:
+        (WebKit::WebPage::requestDocumentEditingContext):
+
+        Refactor logic that uses CharacterIterator to find rects for each character range into a lambda function, and
+        use this lambda function to handle UIWKDocumentRequestRects and the new UIWKDocumentRequestMarkedTextRects. The
+        marked text range is relative to the start of the context before the selection.
+
 2019-11-15  Eric Carlson  <eric.carlson@apple.com>
 
         Don't use AVCapture on watchOS and tvOS
index 9816679..c88de95 100644 (file)
@@ -1110,6 +1110,8 @@ typedef NS_OPTIONS(NSInteger, UIWKDocumentRequestFlags) {
 
 #endif // USE(APPLE_INTERNAL_SDK)
 
+#define UIWKDocumentRequestMarkedTextRects (1 << 5)
+
 #if HAVE(LINK_PREVIEW) && USE(UICONTEXTMENU)
 @interface UIContextMenuConfiguration (IPI)
 @property (nonatomic, copy) UIContextMenuContentPreviewProvider previewProvider;
index c193cf3..9a24a4d 100644 (file)
@@ -47,6 +47,7 @@ struct DocumentEditingContextRequest {
         Rects = 1 << 2,
         Spatial = 1 << 3,
         Annotation = 1 << 4,
+        MarkedTextRects = 1 << 5,
     };
 
     OptionSet<Options> options;
index 2d03fc0..3129595 100644 (file)
@@ -7137,6 +7137,8 @@ static inline OptionSet<WebKit::DocumentEditingContextRequest::Options> toWebDoc
         options.add(WebKit::DocumentEditingContextRequest::Options::Spatial);
     if (flags & UIWKDocumentRequestAnnotation)
         options.add(WebKit::DocumentEditingContextRequest::Options::Annotation);
+    if (flags & UIWKDocumentRequestMarkedTextRects)
+        options.add(WebKit::DocumentEditingContextRequest::Options::MarkedTextRects);
 
     return options;
 }
index 325abfa..3581d1b 100644 (file)
@@ -3936,6 +3936,7 @@ void WebPage::requestDocumentEditingContext(DocumentEditingContextRequest reques
 
     bool isSpatialRequest = request.options.contains(DocumentEditingContextRequest::Options::Spatial);
     bool wantsRects = request.options.contains(DocumentEditingContextRequest::Options::Rects);
+    bool wantsMarkedTextRects = request.options.contains(DocumentEditingContextRequest::Options::MarkedTextRects);
 
     if (auto textInputContext = request.textInputContext) {
         RefPtr<Element> element = elementForContext(*textInputContext);
@@ -4044,9 +4045,10 @@ void WebPage::requestDocumentEditingContext(DocumentEditingContextRequest reques
         context.selectedRangeInMarkedText.length = [context.selectedText.string length];
     }
 
-    if (wantsRects) {
-        CharacterIterator contextIterator(contextBeforeStart.deepEquivalent(), contextAfterEnd.deepEquivalent());
-        unsigned currentLocation = 0;
+    auto characterRectsForRange = [&] (Range& range, uint64_t locationOffset) {
+        Vector<DocumentEditingContext::TextRectAndRange> rects;
+        CharacterIterator contextIterator(range);
+        unsigned currentLocation = locationOffset;
         while (!contextIterator.atEnd()) {
             unsigned length = contextIterator.text().length();
             if (!length) {
@@ -4054,14 +4056,19 @@ void WebPage::requestDocumentEditingContext(DocumentEditingContextRequest reques
                 continue;
             }
 
-            DocumentEditingContext::TextRectAndRange rect;
-            rect.rect = contextIterator.range()->absoluteBoundingBox();
-            rect.range = { currentLocation, 1 };
-            context.textRects.append(rect);
-
+            rects.append({ contextIterator.range()->absoluteBoundingBox(), { currentLocation, 1 } });
             currentLocation++;
             contextIterator.advance(1);
         }
+        return rects;
+    };
+
+    if (wantsRects) {
+        if (auto contextRange = makeRange(contextBeforeStart, contextAfterEnd))
+            context.textRects = characterRectsForRange(*contextRange, 0);
+    } else if (wantsMarkedTextRects && compositionRange) {
+        auto compositionStartOffset = plainTextReplacingNoBreakSpace(contextBeforeStart.deepEquivalent(), compositionRange->startPosition()).length();
+        context.textRects = characterRectsForRange(*compositionRange, compositionStartOffset);
     }
 
 #if ENABLE(PLATFORM_DRIVEN_TEXT_CHECKING)
index d3542bf..657d3bf 100644 (file)
@@ -1,3 +1,17 @@
+2019-11-16  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] [UIWKDocumentContext] Add the ability to request all marked text rects
+        https://bugs.webkit.org/show_bug.cgi?id=204278
+        <rdar://problem/56527803>
+
+        Reviewed by Tim Horton.
+
+        Add an API test to exercise the new request option.
+
+        * TestWebKitAPI/Tests/WebKitCocoa/DocumentEditingContext.mm:
+        (-[UIWKDocumentContext markedTextRects]):
+        * TestWebKitAPI/ios/UIKitSPI.h:
+
 2019-11-15  Eric Carlson  <eric.carlson@apple.com>
 
         [iOS] Audio capture fails when a track is unmuted
index bc33839..161da75 100644 (file)
@@ -62,6 +62,25 @@ static UIWKDocumentRequest *makeRequest(UIWKDocumentRequestFlags flags, UITextGr
     return request.autorelease();
 }
 
+@interface UIWKDocumentContext (TestRunner)
+@property (nonatomic, readonly) NSArray<NSValue *> *markedTextRects;
+@end
+
+@implementation UIWKDocumentContext (TestRunner)
+
+- (NSArray<NSValue *> *)markedTextRects
+{
+    // This should ideally be equivalent to [self characterRectsForCharacterRange:self.markedTextRange]. However, the implementation
+    // of -characterRectsForCharacterRange: in UIKit doesn't guarantee any order to the returned character rects.
+    NSRange range = self.markedTextRange;
+    NSMutableArray *rects = [NSMutableArray arrayWithCapacity:range.length];
+    for (auto location = range.location; location < range.location + range.length; ++location)
+        [rects addObject:[self characterRectsForCharacterRange:NSMakeRange(location, 1)].firstObject];
+    return rects;
+}
+
+@end
+
 @implementation TestWKWebView (SynchronousDocumentContext)
 
 - (UIWKDocumentContext *)synchronouslyRequestDocumentContext:(UIWKDocumentRequest *)request
@@ -234,4 +253,102 @@ TEST(WebKit, DocumentEditingContext)
     EXPECT_NSSTRING_EQ(" world", context.contextAfter);
 }
 
+TEST(WebKit, DocumentEditingContextWithMarkedText)
+{
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]);
+    auto *contentView = [webView textInputContentView];
+
+    [webView synchronouslyLoadHTMLString:@"<body style='-webkit-text-size-adjust: none;' contenteditable>"];
+    [webView evaluateJavaScript:@"document.body.focus()" completionHandler:nil];
+    [webView _synchronouslyExecuteEditCommand:@"InsertText" argument:@"Hello world"];
+
+    [contentView selectWordBackward];
+    [contentView setMarkedText:@"world" selectedRange:NSMakeRange(0, 5)];
+    [webView waitForNextPresentationUpdate];
+    {
+        UIWKDocumentContext *context = [webView synchronouslyRequestDocumentContext:makeRequest(UIWKDocumentRequestText | UIWKDocumentRequestMarkedTextRects, UITextGranularityCharacter, 0)];
+        EXPECT_NULL(context.contextBefore);
+        EXPECT_NULL(context.contextAfter);
+        EXPECT_NSSTRING_EQ("world", context.selectedText);
+        EXPECT_NSSTRING_EQ("world", context.markedText);
+        EXPECT_EQ(0U, context.markedTextRange.location);
+        EXPECT_EQ(5U, context.markedTextRange.length);
+
+        NSArray<NSValue *> *rectValues = context.markedTextRects;
+        EXPECT_EQ(5U, rectValues.count);
+        if (rectValues.count >= 5) {
+            EXPECT_EQ(CGRectMake(47, 8, 13, 19), [rectValues[0] CGRectValue]);
+            EXPECT_EQ(CGRectMake(59, 8, 9, 19), [rectValues[1] CGRectValue]);
+            EXPECT_EQ(CGRectMake(67, 8, 6, 19), [rectValues[2] CGRectValue]);
+            EXPECT_EQ(CGRectMake(72, 8, 5, 19), [rectValues[3] CGRectValue]);
+            EXPECT_EQ(CGRectMake(76, 8, 9, 19), [rectValues[4] CGRectValue]);
+        }
+    }
+    [contentView unmarkText];
+    [webView stringByEvaluatingJavaScript:@"getSelection().setBaseAndExtent(document.body.childNodes[0], 0, document.body.childNodes[0], 5)"];
+    [contentView setMarkedText:@"Hello" selectedRange:NSMakeRange(0, 5)];
+    [webView waitForNextPresentationUpdate];
+    {
+        UIWKDocumentContext *context = [webView synchronouslyRequestDocumentContext:makeRequest(UIWKDocumentRequestText | UIWKDocumentRequestMarkedTextRects, UITextGranularityParagraph, 1)];
+        EXPECT_NULL(context.contextBefore);
+        EXPECT_NSSTRING_EQ(" world", context.contextAfter);
+        EXPECT_NSSTRING_EQ("Hello", context.selectedText);
+        EXPECT_NSSTRING_EQ("Hello", context.markedText);
+        EXPECT_EQ(0U, context.markedTextRange.location);
+        EXPECT_EQ(5U, context.markedTextRange.length);
+
+        NSArray<NSValue *> *rectValues = context.markedTextRects;
+        EXPECT_EQ(5U, rectValues.count);
+        if (rectValues.count >= 5) {
+            EXPECT_EQ(CGRectMake(8, 8, 12, 19), [rectValues[0] CGRectValue]);
+            EXPECT_EQ(CGRectMake(19, 8, 8, 19), [rectValues[1] CGRectValue]);
+            EXPECT_EQ(CGRectMake(26, 8, 6, 19), [rectValues[2] CGRectValue]);
+            EXPECT_EQ(CGRectMake(31, 8, 5, 19), [rectValues[3] CGRectValue]);
+            EXPECT_EQ(CGRectMake(35, 8, 9, 19), [rectValues[4] CGRectValue]);
+        }
+    }
+    [contentView unmarkText];
+    [webView selectAll:nil];
+    [contentView setMarkedText:@"foo" selectedRange:NSMakeRange(0, 3)];
+    [webView waitForNextPresentationUpdate];
+    {
+        UIWKDocumentContext *context = [webView synchronouslyRequestDocumentContext:makeRequest(UIWKDocumentRequestText | UIWKDocumentRequestMarkedTextRects, UITextGranularitySentence, 1)];
+        EXPECT_NULL(context.contextBefore);
+        EXPECT_NULL(context.contextAfter);
+        EXPECT_NSSTRING_EQ("foo", context.selectedText);
+        EXPECT_NSSTRING_EQ("foo", context.markedText);
+        EXPECT_EQ(0U, context.markedTextRange.location);
+        EXPECT_EQ(3U, context.markedTextRange.length);
+
+        NSArray<NSValue *> *rectValues = context.markedTextRects;
+        EXPECT_EQ(3U, rectValues.count);
+        if (rectValues.count >= 3) {
+            EXPECT_EQ(CGRectMake(8, 8, 6, 19), [rectValues[0] CGRectValue]);
+            EXPECT_EQ(CGRectMake(13, 8, 9, 19), [rectValues[1] CGRectValue]);
+            EXPECT_EQ(CGRectMake(21, 8, 9, 19), [rectValues[2] CGRectValue]);
+        }
+    }
+    [contentView unmarkText];
+    [webView collapseToEnd];
+    [contentView setMarkedText:@"bar" selectedRange:NSMakeRange(0, 3)];
+    [webView waitForNextPresentationUpdate];
+    {
+        UIWKDocumentContext *context = [webView synchronouslyRequestDocumentContext:makeRequest(UIWKDocumentRequestText | UIWKDocumentRequestMarkedTextRects, UITextGranularityWord, 1)];
+        EXPECT_NSSTRING_EQ("foo", context.contextBefore);
+        EXPECT_NULL(context.contextAfter);
+        EXPECT_NSSTRING_EQ("bar", context.selectedText);
+        EXPECT_NSSTRING_EQ("bar", context.markedText);
+        EXPECT_EQ(3U, context.markedTextRange.location);
+        EXPECT_EQ(3U, context.markedTextRange.length);
+
+        NSArray<NSValue *> *rectValues = context.markedTextRects;
+        EXPECT_EQ(3U, rectValues.count);
+        if (rectValues.count >= 3) {
+            EXPECT_EQ(CGRectMake(29, 8, 9, 19), [rectValues[0] CGRectValue]);
+            EXPECT_EQ(CGRectMake(37, 8, 8, 19), [rectValues[1] CGRectValue]);
+            EXPECT_EQ(CGRectMake(44, 8, 6, 19), [rectValues[2] CGRectValue]);
+        }
+    }
+}
+
 #endif // PLATFORM(IOS_FAMILY) && HAVE(UI_WK_DOCUMENT_CONTEXT)
index 6f5c725..d5d749f 100644 (file)
@@ -136,6 +136,7 @@ WTF_EXTERN_C_END
 @property (nonatomic, copy) NSObject *markedText;
 @property (nonatomic, assign) NSRange selectedRangeInMarkedText;
 @property (nonatomic, copy) NSAttributedString *annotatedText;
+@property (nonatomic, readonly) NSRange markedTextRange;
 
 - (NSArray<NSValue *> *)characterRectsForCharacterRange:(NSRange)range;
 
@@ -170,6 +171,7 @@ typedef NS_OPTIONS(NSInteger, UIWKDocumentRequestFlags) {
 - (void)pasteWithCompletionHandler:(void (^)(void))completionHandler;
 - (void)requestAutocorrectionRectsForString:(NSString *)input withCompletionHandler:(void (^)(UIWKAutocorrectionRects *rectsForInput))completionHandler;
 - (void)requestAutocorrectionContextWithCompletionHandler:(void (^)(UIWKAutocorrectionContext *autocorrectionContext))completionHandler;
+- (void)selectWordBackward;
 @property (nonatomic, readonly) NSString *selectedText;
 @end
 
@@ -194,6 +196,8 @@ IGNORE_WARNINGS_END
 
 #endif // USE(APPLE_INTERNAL_SDK)
 
+#define UIWKDocumentRequestMarkedTextRects (1 << 5)
+
 @interface UITextAutofillSuggestion ()
 + (instancetype)autofillSuggestionWithUsername:(NSString *)username password:(NSString *)password;
 @end