[WK2][iOS] editorState() should not cause a synchronous layout
authorcdumez@apple.com <cdumez@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 9 Apr 2015 18:31:51 +0000 (18:31 +0000)
committercdumez@apple.com <cdumez@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 9 Apr 2015 18:31:51 +0000 (18:31 +0000)
https://bugs.webkit.org/show_bug.cgi?id=142536
<rdar://problem/20041506>

Reviewed by Enrica Casucci.

Source/WebCore:

Add didChangeSelectionAndUpdateLayout() callback to EditorClient
that is called at the end of FrameSelection::updateAndRevealSelection().

* editing/FrameSelection.cpp:
(WebCore::FrameSelection::updateAndRevealSelection):
* loader/EmptyClients.h:
* page/EditorClient.h:

Source/WebKit/mac:

Provide implementation for EditorClient::didChangeSelectionAndUpdateLayout().

* WebCoreSupport/WebEditorClient.h:

Source/WebKit/win:

Provide implementation for EditorClient::didChangeSelectionAndUpdateLayout().

* WebCoreSupport/WebEditorClient.h:

Source/WebKit2:

platformEditorState() on iOS does a synchronous layout to compute some
of the EditorState members (e.g. caretRectAtStart / caretRectAtEnd).
This is bad for performance as this is called every time the selection
is changed (which happens for e.g. when you set the value of a focused
HTMLInputElement).

This patch updates the behavior on iOS to only send a partial EditorState
on selection change so that the UIProcess gets most of the information
(the ones that do not require style recalc or layout) ASAP. A full Editor
state is then sent after the asynchronous layout is done.

With this change, I see a 38% improvement on Speedometer (26.4 +/- 0.37
-> 36.5 +/- 0.54) on iPhone 6 Plus.

* Shared/EditorState.cpp:
(WebKit::EditorState::encode):
(WebKit::EditorState::decode):
(WebKit::EditorState::PostLayoutData::encode):
(WebKit::EditorState::PostLayoutData::decode):
* Shared/EditorState.h:
(WebKit::EditorState::EditorState): Deleted.
* UIProcess/ios/WKContentView.mm:
(-[WKContentView _didCommitLayerTree:]):
* UIProcess/ios/WKContentViewInteraction.mm:
(WebKit::WKSelectionDrawingInfo::WKSelectionDrawingInfo):
(-[WKContentView webSelectionRects]):
(-[WKContentView _addShortcut:]):
(-[WKContentView selectedText]):
(-[WKContentView isReplaceAllowed]):
(-[WKContentView _promptForReplace:]):
(-[WKContentView _transliterateChinese:]):
(-[WKContentView textStylingAtPosition:inDirection:]):
(-[WKContentView canPerformAction:withSender:]):
(-[WKContentView _showDictionary:]):
(-[WKContentView _characterBeforeCaretSelection]):
(-[WKContentView _characterInRelationToCaretSelection:]):
(-[WKContentView _selectionAtDocumentStart]):
(-[WKContentView selectedTextRange]):
(-[WKContentView hasContent]):
* WebProcess/WebCoreSupport/WebEditorClient.cpp:
(WebKit::WebEditorClient::didChangeSelectionAndUpdateLayout):
* WebProcess/WebCoreSupport/WebEditorClient.h:
* WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::editorState):
(WebKit::WebPage::didChangeSelection):
(WebKit::WebPage::sendPostLayoutEditorStateIfNeeded):
* WebProcess/WebPage/WebPage.h:
* WebProcess/WebPage/efl/WebPageEfl.cpp:
(WebKit::WebPage::platformEditorState):
* WebProcess/WebPage/gtk/WebPageGtk.cpp:
(WebKit::WebPage::platformEditorState):
* WebProcess/WebPage/ios/WebPageIOS.mm:
(WebKit::WebPage::platformEditorState):
* WebProcess/WebPage/mac/WebPageMac.mm:
(WebKit::WebPage::platformEditorState):

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

21 files changed:
Source/WebCore/ChangeLog
Source/WebCore/editing/FrameSelection.cpp
Source/WebCore/loader/EmptyClients.h
Source/WebCore/page/EditorClient.h
Source/WebKit/mac/ChangeLog
Source/WebKit/mac/WebCoreSupport/WebEditorClient.h
Source/WebKit/win/ChangeLog
Source/WebKit/win/WebCoreSupport/WebEditorClient.h
Source/WebKit2/ChangeLog
Source/WebKit2/Shared/EditorState.cpp
Source/WebKit2/Shared/EditorState.h
Source/WebKit2/UIProcess/ios/WKContentView.mm
Source/WebKit2/UIProcess/ios/WKContentViewInteraction.mm
Source/WebKit2/WebProcess/WebCoreSupport/WebEditorClient.cpp
Source/WebKit2/WebProcess/WebCoreSupport/WebEditorClient.h
Source/WebKit2/WebProcess/WebPage/WebPage.cpp
Source/WebKit2/WebProcess/WebPage/WebPage.h
Source/WebKit2/WebProcess/WebPage/efl/WebPageEfl.cpp
Source/WebKit2/WebProcess/WebPage/gtk/WebPageGtk.cpp
Source/WebKit2/WebProcess/WebPage/ios/WebPageIOS.mm
Source/WebKit2/WebProcess/WebPage/mac/WebPageMac.mm

index c278a0650e72b9d6c24a310108995c4f3c7e3b78..7ef58b572e75bff645a739ae37b1d59b18292677 100644 (file)
@@ -1,3 +1,19 @@
+2015-04-09  Chris Dumez  <cdumez@apple.com>
+
+        [WK2][iOS] editorState() should not cause a synchronous layout
+        https://bugs.webkit.org/show_bug.cgi?id=142536
+        <rdar://problem/20041506>
+
+        Reviewed by Enrica Casucci.
+
+        Add didChangeSelectionAndUpdateLayout() callback to EditorClient
+        that is called at the end of FrameSelection::updateAndRevealSelection().
+
+        * editing/FrameSelection.cpp:
+        (WebCore::FrameSelection::updateAndRevealSelection):
+        * loader/EmptyClients.h:
+        * page/EditorClient.h:
+
 2015-04-08  Anders Carlsson  <andersca@apple.com>
 
         Give each cache group a storage and use it in place of the singleton
index a8f1b41773d2a833e45c7be0769332655eabec23..e39bfc616fb489d7f3fd74c26939bc92a62b1f10 100644 (file)
@@ -381,6 +381,8 @@ void FrameSelection::updateAndRevealSelection()
     }
 
     notifyAccessibilityForSelectionChange();
+
+    m_frame->editor().client()->didChangeSelectionAndUpdateLayout();
 }
 
 void FrameSelection::updateDataDetectorsForSelection()
index ce8c240b73f1b826df715a4de750e78c58092a62..9fd11dd0b2aecb07be2bdd7cdcfff14351c0e6e2 100644 (file)
@@ -446,6 +446,7 @@ public:
     virtual void didBeginEditing() override { }
     virtual void respondToChangedContents() override { }
     virtual void respondToChangedSelection(Frame*) override { }
+    virtual void didChangeSelectionAndUpdateLayout() override { }
     virtual void discardedComposition(Frame*) override { }
     virtual void didEndEditing() override { }
     virtual void willWriteSelectionToPasteboard(Range*) override { }
index e18c027a583d6a5536e0cc7de55931a164d92110..2d551c7d452c2a9bbe6539835691bee7d015fdbe 100644 (file)
@@ -93,6 +93,7 @@ public:
     virtual void didBeginEditing() = 0;
     virtual void respondToChangedContents() = 0;
     virtual void respondToChangedSelection(Frame*) = 0;
+    virtual void didChangeSelectionAndUpdateLayout() = 0;
     virtual void didEndEditing() = 0;
     virtual void willWriteSelectionToPasteboard(Range*) = 0;
     virtual void didWriteSelectionToPasteboard() = 0;
index 66cf7233e135d8f30805a27424fc58ead1ac12e2..d550198d1b77477f5b0168c864e9d46252c35639 100644 (file)
@@ -1,3 +1,15 @@
+2015-04-09  Chris Dumez  <cdumez@apple.com>
+
+        [WK2][iOS] editorState() should not cause a synchronous layout
+        https://bugs.webkit.org/show_bug.cgi?id=142536
+        <rdar://problem/20041506>
+
+        Reviewed by Enrica Casucci.
+
+        Provide implementation for EditorClient::didChangeSelectionAndUpdateLayout().
+
+        * WebCoreSupport/WebEditorClient.h:
+
 2015-04-08  Brent Fulgham  <bfulgham@apple.com>
 
         [Mac] WebKit is not honoring OS preferences for secondary click behaviors
index 90e166aa74443484069932b6acc7243a58768769..64780389a4e37c2818f2b243cfbeb8b1a3df879c 100644 (file)
@@ -110,6 +110,7 @@ private:
 
     virtual void respondToChangedContents() override;
     virtual void respondToChangedSelection(WebCore::Frame*) override;
+    virtual void didChangeSelectionAndUpdateLayout() override { }
     virtual void discardedComposition(WebCore::Frame*) override;
 
     virtual void registerUndoStep(PassRefPtr<WebCore::UndoStep>) override;
index 2b118491e9c43ff7b6e6ac49b2209ed2f89811cd..a55f0d5226cbb0d5eb35d2eaf9cf2aacc39e5fa1 100644 (file)
@@ -1,3 +1,15 @@
+2015-04-09  Chris Dumez  <cdumez@apple.com>
+
+        [WK2][iOS] editorState() should not cause a synchronous layout
+        https://bugs.webkit.org/show_bug.cgi?id=142536
+        <rdar://problem/20041506>
+
+        Reviewed by Enrica Casucci.
+
+        Provide implementation for EditorClient::didChangeSelectionAndUpdateLayout().
+
+        * WebCoreSupport/WebEditorClient.h:
+
 2015-04-08  Brady Eidson  <beidson@apple.com>
 
         Expose the "Share" menu for links, images, and media.
index 481376102426418bd22e0be94b4131541c154f20..15a0027653697b17a68a36fb6e0c9c094eeaee10 100644 (file)
@@ -60,6 +60,7 @@ public:
 
     virtual void respondToChangedContents();
     virtual void respondToChangedSelection(WebCore::Frame*);
+    virtual void didChangeSelectionAndUpdateLayout() override { }
     virtual void discardedComposition(WebCore::Frame*) override;
 
     bool shouldDeleteRange(WebCore::Range*);
index 4ae7ef37492353fd0ba1e278ab316936b231894b..286d15d0a2c5d690973979bd0041d313622ba4d9 100644 (file)
@@ -1,3 +1,67 @@
+2015-04-09  Chris Dumez  <cdumez@apple.com>
+
+        [WK2][iOS] editorState() should not cause a synchronous layout
+        https://bugs.webkit.org/show_bug.cgi?id=142536
+        <rdar://problem/20041506>
+
+        Reviewed by Enrica Casucci.
+
+        platformEditorState() on iOS does a synchronous layout to compute some
+        of the EditorState members (e.g. caretRectAtStart / caretRectAtEnd).
+        This is bad for performance as this is called every time the selection
+        is changed (which happens for e.g. when you set the value of a focused
+        HTMLInputElement).
+
+        This patch updates the behavior on iOS to only send a partial EditorState
+        on selection change so that the UIProcess gets most of the information
+        (the ones that do not require style recalc or layout) ASAP. A full Editor
+        state is then sent after the asynchronous layout is done.
+
+        With this change, I see a 38% improvement on Speedometer (26.4 +/- 0.37
+        -> 36.5 +/- 0.54) on iPhone 6 Plus.
+
+        * Shared/EditorState.cpp:
+        (WebKit::EditorState::encode):
+        (WebKit::EditorState::decode):
+        (WebKit::EditorState::PostLayoutData::encode):
+        (WebKit::EditorState::PostLayoutData::decode):
+        * Shared/EditorState.h:
+        (WebKit::EditorState::EditorState): Deleted.
+        * UIProcess/ios/WKContentView.mm:
+        (-[WKContentView _didCommitLayerTree:]):
+        * UIProcess/ios/WKContentViewInteraction.mm:
+        (WebKit::WKSelectionDrawingInfo::WKSelectionDrawingInfo):
+        (-[WKContentView webSelectionRects]):
+        (-[WKContentView _addShortcut:]):
+        (-[WKContentView selectedText]):
+        (-[WKContentView isReplaceAllowed]):
+        (-[WKContentView _promptForReplace:]):
+        (-[WKContentView _transliterateChinese:]):
+        (-[WKContentView textStylingAtPosition:inDirection:]):
+        (-[WKContentView canPerformAction:withSender:]):
+        (-[WKContentView _showDictionary:]):
+        (-[WKContentView _characterBeforeCaretSelection]):
+        (-[WKContentView _characterInRelationToCaretSelection:]):
+        (-[WKContentView _selectionAtDocumentStart]):
+        (-[WKContentView selectedTextRange]):
+        (-[WKContentView hasContent]):
+        * WebProcess/WebCoreSupport/WebEditorClient.cpp:
+        (WebKit::WebEditorClient::didChangeSelectionAndUpdateLayout):
+        * WebProcess/WebCoreSupport/WebEditorClient.h:
+        * WebProcess/WebPage/WebPage.cpp:
+        (WebKit::WebPage::editorState):
+        (WebKit::WebPage::didChangeSelection):
+        (WebKit::WebPage::sendPostLayoutEditorStateIfNeeded):
+        * WebProcess/WebPage/WebPage.h:
+        * WebProcess/WebPage/efl/WebPageEfl.cpp:
+        (WebKit::WebPage::platformEditorState):
+        * WebProcess/WebPage/gtk/WebPageGtk.cpp:
+        (WebKit::WebPage::platformEditorState):
+        * WebProcess/WebPage/ios/WebPageIOS.mm:
+        (WebKit::WebPage::platformEditorState):
+        * WebProcess/WebPage/mac/WebPageMac.mm:
+        (WebKit::WebPage::platformEditorState):
+
 2015-04-09  Antti Koivisto  <antti@apple.com>
 
         Network Cache: Crash in WebCore::CachedResource::tryReplaceEncodedData
index 9bc0b8fc1a8999eaface415c6dd94c102414dd20..c15ff7afe693f561df550f91525b970dafa28adc 100644 (file)
@@ -41,22 +41,15 @@ void EditorState::encode(IPC::ArgumentEncoder& encoder) const
     encoder << isInPasswordField;
     encoder << isInPlugin;
     encoder << hasComposition;
+    encoder << isMissingPostLayoutData;
 
 #if PLATFORM(IOS)
-    encoder << isReplaceAllowed;
-    encoder << hasContent;
-    encoder << characterAfterSelection;
-    encoder << characterBeforeSelection;
-    encoder << twoCharacterBeforeSelection;
-    encoder << caretRectAtStart;
-    encoder << caretRectAtEnd;
-    encoder << selectionRects;
-    encoder << selectedTextLength;
-    encoder << wordAtSelection;
+    if (!isMissingPostLayoutData)
+        m_postLayoutData.encode(encoder);
+
     encoder << firstMarkedRect;
     encoder << lastMarkedRect;
     encoder << markedText;
-    encoder << typingAttributes;
 #endif
 
 #if PLATFORM(GTK)
@@ -90,43 +83,74 @@ bool EditorState::decode(IPC::ArgumentDecoder& decoder, EditorState& result)
     if (!decoder.decode(result.hasComposition))
         return false;
 
+    if (!decoder.decode(result.isMissingPostLayoutData))
+        return false;
+
 #if PLATFORM(IOS)
-    if (!decoder.decode(result.isReplaceAllowed))
+    if (!result.isMissingPostLayoutData) {
+        if (!PostLayoutData::decode(decoder, result.postLayoutData()))
+            return false;
+    }
+
+    if (!decoder.decode(result.firstMarkedRect))
         return false;
-    if (!decoder.decode(result.hasContent))
+    if (!decoder.decode(result.lastMarkedRect))
         return false;
-    if (!decoder.decode(result.characterAfterSelection))
+    if (!decoder.decode(result.markedText))
         return false;
-    if (!decoder.decode(result.characterBeforeSelection))
+#endif
+
+#if PLATFORM(GTK)
+    if (!decoder.decode(result.cursorRect))
         return false;
-    if (!decoder.decode(result.twoCharacterBeforeSelection))
+#endif
+
+    return true;
+}
+
+#if PLATFORM(IOS)
+void EditorState::PostLayoutData::encode(IPC::ArgumentEncoder& encoder) const
+{
+    encoder << selectionRects;
+    encoder << caretRectAtStart;
+    encoder << caretRectAtEnd;
+    encoder << wordAtSelection;
+    encoder << selectedTextLength;
+    encoder << characterAfterSelection;
+    encoder << characterBeforeSelection;
+    encoder << twoCharacterBeforeSelection;
+    encoder << typingAttributes;
+    encoder << isReplaceAllowed;
+    encoder << hasContent;
+}
+
+bool EditorState::PostLayoutData::decode(IPC::ArgumentDecoder& decoder, PostLayoutData& result)
+{
+    if (!decoder.decode(result.selectionRects))
         return false;
     if (!decoder.decode(result.caretRectAtStart))
         return false;
     if (!decoder.decode(result.caretRectAtEnd))
         return false;
-    if (!decoder.decode(result.selectionRects))
+    if (!decoder.decode(result.wordAtSelection))
         return false;
     if (!decoder.decode(result.selectedTextLength))
         return false;
-    if (!decoder.decode(result.wordAtSelection))
-        return false;
-    if (!decoder.decode(result.firstMarkedRect))
+    if (!decoder.decode(result.characterAfterSelection))
         return false;
-    if (!decoder.decode(result.lastMarkedRect))
+    if (!decoder.decode(result.characterBeforeSelection))
         return false;
-    if (!decoder.decode(result.markedText))
+    if (!decoder.decode(result.twoCharacterBeforeSelection))
         return false;
     if (!decoder.decode(result.typingAttributes))
         return false;
-#endif
-
-#if PLATFORM(GTK)
-    if (!decoder.decode(result.cursorRect))
+    if (!decoder.decode(result.isReplaceAllowed))
+        return false;
+    if (!decoder.decode(result.hasContent))
         return false;
-#endif
 
     return true;
 }
+#endif
 
 }
index 979ad5b1efb8fc74ef5a7a7f8fc7d663afbcce1a..4732066bfaf4c10293fa64ffabfe6a653beabf8e 100644 (file)
@@ -44,52 +44,41 @@ enum TypingAttributes {
 };
 
 struct EditorState {
-    EditorState()
-        : shouldIgnoreCompositionSelectionChange(false)
-        , selectionIsNone(true)
-        , selectionIsRange(false)
-        , isContentEditable(false)
-        , isContentRichlyEditable(false)
-        , isInPasswordField(false)
-        , isInPlugin(false)
-        , hasComposition(false)
-#if PLATFORM(IOS)
-        , isReplaceAllowed(false)
-        , hasContent(false)
-        , characterAfterSelection(0)
-        , characterBeforeSelection(0)
-        , twoCharacterBeforeSelection(0)
-        , selectedTextLength(0)
-        , typingAttributes(AttributeNone)
-#endif
-    {
-    }
+    bool shouldIgnoreCompositionSelectionChange { false };
 
-    bool shouldIgnoreCompositionSelectionChange;
-
-    bool selectionIsNone; // This will be false when there is a caret selection.
-    bool selectionIsRange;
-    bool isContentEditable;
-    bool isContentRichlyEditable;
-    bool isInPasswordField;
-    bool isInPlugin;
-    bool hasComposition;
+    bool selectionIsNone { true }; // This will be false when there is a caret selection.
+    bool selectionIsRange { false };
+    bool isContentEditable { false };
+    bool isContentRichlyEditable { false };
+    bool isInPasswordField { false };
+    bool isInPlugin { false };
+    bool hasComposition { false };
+    bool isMissingPostLayoutData { false };
 
 #if PLATFORM(IOS)
-    bool isReplaceAllowed;
-    bool hasContent;
-    UChar32 characterAfterSelection;
-    UChar32 characterBeforeSelection;
-    UChar32 twoCharacterBeforeSelection;
-    WebCore::IntRect caretRectAtStart;
-    WebCore::IntRect caretRectAtEnd;
-    Vector<WebCore::SelectionRect> selectionRects;
-    uint64_t selectedTextLength;
-    String wordAtSelection;
     WebCore::IntRect firstMarkedRect;
     WebCore::IntRect lastMarkedRect;
     String markedText;
-    uint32_t typingAttributes;
+
+    struct PostLayoutData {
+        Vector<WebCore::SelectionRect> selectionRects;
+        WebCore::IntRect caretRectAtStart;
+        WebCore::IntRect caretRectAtEnd;
+        String wordAtSelection;
+        uint64_t selectedTextLength { 0 };
+        UChar32 characterAfterSelection { 0 };
+        UChar32 characterBeforeSelection { 0 };
+        UChar32 twoCharacterBeforeSelection { 0 };
+        uint32_t typingAttributes { AttributeNone };
+        bool isReplaceAllowed { false };
+        bool hasContent { false };
+
+        void encode(IPC::ArgumentEncoder&) const;
+        static bool decode(IPC::ArgumentDecoder&, PostLayoutData&);
+    };
+
+    const PostLayoutData& postLayoutData() const;
+    PostLayoutData& postLayoutData();
 #endif
 
 #if PLATFORM(GTK)
@@ -98,8 +87,27 @@ struct EditorState {
 
     void encode(IPC::ArgumentEncoder&) const;
     static bool decode(IPC::ArgumentDecoder&, EditorState&);
+
+#if PLATFORM(IOS)
+private:
+    PostLayoutData m_postLayoutData;
+#endif
 };
 
+#if PLATFORM(IOS)
+inline auto EditorState::postLayoutData() -> PostLayoutData&
+{
+    ASSERT_WITH_MESSAGE(!isMissingPostLayoutData, "Attempt to access post layout data before receiving it");
+    return m_postLayoutData;
+}
+
+inline auto EditorState::postLayoutData() const -> const PostLayoutData&
+{
+    ASSERT_WITH_MESSAGE(!isMissingPostLayoutData, "Attempt to access post layout data before receiving it");
+    return m_postLayoutData;
+}
+#endif
+
 }
 
 #endif // EditorState_h
index c15af20cc75e4ee0ce4b2deb246660d27d8cf806..fc0ce4486927cf11c940fd4d8cb5ad8950979d14 100644 (file)
@@ -490,7 +490,11 @@ private:
         [_webView _updateVisibleContentRects];
     }
     
-    [self _updateChangedSelection];
+    // Updating the selection requires a full editor state. If the editor state is missing post layout
+    // data then it means there is a layout pending and we're going to be called again after the layout
+    // so we delay the selection update.
+    if (!_page->editorState().isMissingPostLayoutData)
+        [self _updateChangedSelection];
 }
 
 - (void)_setAcceleratedCompositingRootView:(UIView *)rootView
index 8f1d755aa7ec2dbbe00333eb50235842ed923fa9..da72decf7cc00a5e43a0bb989e8a7b01743d9149 100644 (file)
@@ -85,8 +85,9 @@ WKSelectionDrawingInfo::WKSelectionDrawingInfo(const EditorState& editorState)
     }
 
     type = SelectionType::Range;
-    caretRect = editorState.caretRectAtEnd;
-    selectionRects = editorState.selectionRects;
+    auto& postLayoutData = editorState.postLayoutData();
+    caretRect = postLayoutData.caretRectAtEnd;
+    selectionRects = postLayoutData.selectionRects;
 }
 
 inline bool operator==(const WKSelectionDrawingInfo& a, const WKSelectionDrawingInfo& b)
@@ -986,13 +987,14 @@ static inline bool isSamePair(UIGestureRecognizer *a, UIGestureRecognizer *b, UI
 
 - (NSArray *)webSelectionRects
 {
-    unsigned size = _page->editorState().selectionRects.size();
+    const auto& selectionRects = _page->editorState().postLayoutData().selectionRects;
+    unsigned size = selectionRects.size();
     if (!size)
         return nil;
 
     NSMutableArray *webRects = [NSMutableArray arrayWithCapacity:size];
     for (unsigned i = 0; i < size; i++) {
-        const WebCore::SelectionRect& coreRect = _page->editorState().selectionRects[i];
+        const WebCore::SelectionRect& coreRect = selectionRects[i];
         WebSelectionRect *webRect = [WebSelectionRect selectionRect];
         webRect.rect = coreRect.rect();
         webRect.writingDirection = coreRect.direction() == LTR ? WKWritingDirectionLeftToRight : WKWritingDirectionRightToLeft;
@@ -1277,19 +1279,19 @@ static void cancelPotentialTapIfNecessary(WKContentView* contentView)
 - (void)_addShortcut:(id)sender
 {
     if (_textSelectionAssistant && [_textSelectionAssistant respondsToSelector:@selector(showTextServiceFor:fromRect:)])
-        [_textSelectionAssistant showTextServiceFor:[self selectedText] fromRect:_page->editorState().selectionRects[0].rect()];
+        [_textSelectionAssistant showTextServiceFor:[self selectedText] fromRect:_page->editorState().postLayoutData().selectionRects[0].rect()];
     else if (_webSelectionAssistant && [_webSelectionAssistant respondsToSelector:@selector(showTextServiceFor:fromRect:)])
-        [_webSelectionAssistant showTextServiceFor:[self selectedText] fromRect:_page->editorState().selectionRects[0].rect()];
+        [_webSelectionAssistant showTextServiceFor:[self selectedText] fromRect:_page->editorState().postLayoutData().selectionRects[0].rect()];
 }
 
 - (NSString *)selectedText
 {
-    return (NSString *)_page->editorState().wordAtSelection;
+    return (NSString *)_page->editorState().postLayoutData().wordAtSelection;
 }
 
 - (BOOL)isReplaceAllowed
 {
-    return _page->editorState().isReplaceAllowed;
+    return _page->editorState().postLayoutData().isReplaceAllowed;
 }
 
 - (void)replaceText:(NSString *)text withText:(NSString *)word
@@ -1304,17 +1306,18 @@ static void cancelPotentialTapIfNecessary(WKContentView* contentView)
 
 - (void)_promptForReplace:(id)sender
 {
-    if (_page->editorState().wordAtSelection.isEmpty())
+    const auto& wordAtSelection = _page->editorState().postLayoutData().wordAtSelection;
+    if (wordAtSelection.isEmpty())
         return;
 
     if ([_textSelectionAssistant respondsToSelector:@selector(scheduleReplacementsForText:)])
-        [_textSelectionAssistant scheduleReplacementsForText:_page->editorState().wordAtSelection];
+        [_textSelectionAssistant scheduleReplacementsForText:wordAtSelection];
 }
 
 - (void)_transliterateChinese:(id)sender
 {
     if ([_textSelectionAssistant respondsToSelector:@selector(scheduleChineseTransliterationForText:)])
-        [_textSelectionAssistant scheduleChineseTransliterationForText:_page->editorState().wordAtSelection];
+        [_textSelectionAssistant scheduleChineseTransliterationForText:_page->editorState().postLayoutData().wordAtSelection];
 }
 
 - (void)_reanalyze:(id)sender
@@ -1334,10 +1337,11 @@ static void cancelPotentialTapIfNecessary(WKContentView* contentView)
 
     NSMutableDictionary* result = [NSMutableDictionary dictionary];
 
+    auto typingAttributes = _page->editorState().postLayoutData().typingAttributes;
     CTFontSymbolicTraits symbolicTraits = 0;
-    if (_page->editorState().typingAttributes & AttributeBold)
+    if (typingAttributes & AttributeBold)
         symbolicTraits |= kCTFontBoldTrait;
-    if (_page->editorState().typingAttributes & AttributeItalics)
+    if (typingAttributes & AttributeItalics)
         symbolicTraits |= kCTFontTraitItalic;
 
     // We chose a random font family and size.
@@ -1351,7 +1355,7 @@ static void cancelPotentialTapIfNecessary(WKContentView* contentView)
     if (font)
         [result setObject:(id)font.get() forKey:NSFontAttributeName];
     
-    if (_page->editorState().typingAttributes & AttributeUnderline)
+    if (typingAttributes & AttributeUnderline)
         [result setObject:[NSNumber numberWithInt:NSUnderlineStyleSingle] forKey:NSUnderlineStyleAttributeName];
 
     return result;
@@ -1389,7 +1393,7 @@ static void cancelPotentialTapIfNecessary(WKContentView* contentView)
         if (_page->editorState().isInPasswordField || !(hasWebSelection || _page->editorState().selectionIsRange))
             return NO;
 
-        NSUInteger textLength = _page->editorState().selectedTextLength;
+        NSUInteger textLength = _page->editorState().postLayoutData().selectedTextLength;
         // FIXME: We should be calling UIReferenceLibraryViewController to check if the length is
         // acceptable, but the interface takes a string.
         // <rdar://problem/15254406>
@@ -1418,7 +1422,7 @@ static void cancelPotentialTapIfNecessary(WKContentView* contentView)
     }
 
     if (action == @selector(_promptForReplace:)) {
-        if (!_page->editorState().selectionIsRange || !_page->editorState().isReplaceAllowed || ![[UIKeyboardImpl activeInstance] autocorrectSpellingEnabled])
+        if (!_page->editorState().selectionIsRange || !_page->editorState().postLayoutData().isReplaceAllowed || ![[UIKeyboardImpl activeInstance] autocorrectSpellingEnabled])
             return NO;
         if ([[self selectedText] _containsCJScriptsOnly])
             return NO;
@@ -1426,13 +1430,13 @@ static void cancelPotentialTapIfNecessary(WKContentView* contentView)
     }
 
     if (action == @selector(_transliterateChinese:)) {
-        if (!_page->editorState().selectionIsRange || !_page->editorState().isReplaceAllowed || ![[UIKeyboardImpl activeInstance] autocorrectSpellingEnabled])
+        if (!_page->editorState().selectionIsRange || !_page->editorState().postLayoutData().isReplaceAllowed || ![[UIKeyboardImpl activeInstance] autocorrectSpellingEnabled])
             return NO;
         return UIKeyboardEnabledInputModesAllowChineseTransliterationForText([self selectedText]);
     }
 
     if (action == @selector(_reanalyze:)) {
-        if (!_page->editorState().selectionIsRange || !_page->editorState().isReplaceAllowed || ![[UIKeyboardImpl activeInstance] autocorrectSpellingEnabled])
+        if (!_page->editorState().selectionIsRange || !_page->editorState().postLayoutData().isReplaceAllowed || ![[UIKeyboardImpl activeInstance] autocorrectSpellingEnabled])
             return NO;
         return UIKeyboardCurrentInputModeAllowsChineseOrJapaneseReanalysisForText([self selectedText]);
     }
@@ -1530,7 +1534,7 @@ static void cancelPotentialTapIfNecessary(WKContentView* contentView)
 
 - (void)_showDictionary:(NSString *)text
 {
-    CGRect presentationRect = _page->editorState().selectionRects[0].rect();
+    CGRect presentationRect = _page->editorState().postLayoutData().selectionRects[0].rect();
     if (_textSelectionAssistant)
         [_textSelectionAssistant showDictionaryFor:text fromRect:presentationRect];
     else
@@ -1937,18 +1941,18 @@ static void selectionChangedWithTouch(WKContentView *view, const WebCore::IntPoi
 
 - (UTF32Char)_characterBeforeCaretSelection
 {
-    return _page->editorState().characterBeforeSelection;
+    return _page->editorState().postLayoutData().characterBeforeSelection;
 }
 
 - (UTF32Char)_characterInRelationToCaretSelection:(int)amount
 {
     switch (amount) {
     case 0:
-        return _page->editorState().characterAfterSelection;
+        return _page->editorState().postLayoutData().characterAfterSelection;
     case -1:
-        return _page->editorState().characterBeforeSelection;
+        return _page->editorState().postLayoutData().characterBeforeSelection;
     case -2:
-        return _page->editorState().twoCharacterBeforeSelection;
+        return _page->editorState().postLayoutData().twoCharacterBeforeSelection;
     default:
         return 0;
     }
@@ -1956,7 +1960,7 @@ static void selectionChangedWithTouch(WKContentView *view, const WebCore::IntPoi
 
 - (BOOL)_selectionAtDocumentStart
 {
-    return !_page->editorState().characterBeforeSelection;
+    return !_page->editorState().postLayoutData().characterBeforeSelection;
 }
 
 - (CGRect)textFirstRect
@@ -2130,8 +2134,9 @@ static void selectionChangedWithTouch(WKContentView *view, const WebCore::IntPoi
 
 - (UITextRange *)selectedTextRange
 {
-    FloatRect startRect = _page->editorState().caretRectAtStart;
-    FloatRect endRect = _page->editorState().caretRectAtEnd;
+    auto& postLayoutEditorStateData = _page->editorState().postLayoutData();
+    FloatRect startRect = postLayoutEditorStateData.caretRectAtStart;
+    FloatRect endRect = postLayoutEditorStateData.caretRectAtEnd;
     double inverseScale = [self inverseScale];
     // We want to keep the original caret width, while the height scales with
     // the content taking orientation into account.
@@ -2159,7 +2164,7 @@ static void selectionChangedWithTouch(WKContentView *view, const WebCore::IntPoi
                                  startRect:startRect
                                    endRect:endRect
                             selectionRects:[self webSelectionRects]
-                        selectedTextLength:_page->editorState().selectedTextLength];
+                        selectedTextLength:postLayoutEditorStateData.selectedTextLength];
 }
 
 - (CGRect)caretRectForPosition:(UITextPosition *)position
@@ -2680,7 +2685,7 @@ static UITextAutocapitalizationType toUITextAutocapitalize(WebAutocapitalizeType
 
 - (BOOL)hasContent
 {
-    return _page->editorState().hasContent;
+    return _page->editorState().postLayoutData().hasContent;
 }
 
 - (void)selectAll
index 34afbbadfdb4d986f5f7ca3dcdda4703ffe7926f..b8cbcceadf0ba3a47b28babbdde3818331a5970b 100644 (file)
@@ -190,6 +190,11 @@ void WebEditorClient::respondToChangedSelection(Frame* frame)
 #endif
 }
 
+void WebEditorClient::didChangeSelectionAndUpdateLayout()
+{
+    m_page->sendPostLayoutEditorStateIfNeeded();
+}
+
 void WebEditorClient::discardedComposition(Frame*)
 {
     m_page->discardedComposition();
index 5fc613163addd63da0e6b4f577ed6a426082711f..77055194f4e50d7037717b82565e5eb196881cb6 100644 (file)
@@ -64,6 +64,7 @@ private:
     virtual void didBeginEditing() override;
     virtual void respondToChangedContents() override;
     virtual void respondToChangedSelection(WebCore::Frame*) override;
+    virtual void didChangeSelectionAndUpdateLayout() override;
     virtual void discardedComposition(WebCore::Frame*) override;
     virtual void didEndEditing() override;
     virtual void willWriteSelectionToPasteboard(WebCore::Range*) override;
index 97162637ae4c9a674a306e5dbb84663b937a9f68..46d38e5069fceb2d52ad230de4334014eb1ce605 100644 (file)
@@ -739,7 +739,7 @@ WebCore::WebGLLoadPolicy WebPage::resolveWebGLPolicyForURL(WebFrame*, const Stri
 }
 #endif
 
-EditorState WebPage::editorState() const
+EditorState WebPage::editorState(IncludePostLayoutDataHint shouldIncludePostLayoutData) const
 {
     Frame& frame = m_page->focusController().focusedOrMainFrame();
 
@@ -764,7 +764,7 @@ EditorState WebPage::editorState() const
     result.hasComposition = frame.editor().hasComposition();
     result.shouldIgnoreCompositionSelectionChange = frame.editor().ignoreCompositionSelectionChange();
     
-    platformEditorState(frame, result);
+    platformEditorState(frame, result, shouldIncludePostLayoutData);
 
     return result;
 }
@@ -4365,19 +4365,29 @@ void WebPage::cancelComposition()
 
 void WebPage::didChangeSelection()
 {
-#if PLATFORM(MAC) && USE(ASYNC_NSTEXTINPUTCLIENT)
     Frame& frame = m_page->focusController().focusedOrMainFrame();
+    FrameView* view = frame.view();
+    bool needsLayout = view && view->needsLayout();
+
+    // If there is a layout pending, we should avoid populating EditorState that require layout to be done or it will
+    // trigger a synchronous layout every time the selection changes. sendPostLayoutEditorStateIfNeeded() will be called
+    // to send the full editor state after layout is done if we send a partial editor state here.
+    auto editorState = this->editorState(needsLayout ? IncludePostLayoutDataHint::No : IncludePostLayoutDataHint::Yes);
+    ASSERT_WITH_MESSAGE(needsLayout == (view && view->needsLayout()), "Calling editorState() should not cause a synchronous layout.");
+    m_isEditorStateMissingPostLayoutData = editorState.isMissingPostLayoutData;
+
+#if PLATFORM(MAC) && USE(ASYNC_NSTEXTINPUTCLIENT)
     // Abandon the current inline input session if selection changed for any other reason but an input method direct action.
     // FIXME: This logic should be in WebCore.
     // FIXME: Many changes that affect composition node do not go through didChangeSelection(). We need to do something when DOM manipulation affects the composition, because otherwise input method's idea about it will be different from Editor's.
     // FIXME: We can't cancel composition when selection changes to NoSelection, but we probably should.
     if (frame.editor().hasComposition() && !frame.editor().ignoreCompositionSelectionChange() && !frame.selection().isNone()) {
         frame.editor().cancelComposition();
-        send(Messages::WebPageProxy::CompositionWasCanceled(editorState()));
+        send(Messages::WebPageProxy::CompositionWasCanceled(editorState));
     } else
-        send(Messages::WebPageProxy::EditorStateChanged(editorState()));
+        send(Messages::WebPageProxy::EditorStateChanged(editorState));
 #else
-    send(Messages::WebPageProxy::EditorStateChanged(editorState()), pageID(), IPC::DispatchMessageEvenWhenWaitingForSyncReply);
+    send(Messages::WebPageProxy::EditorStateChanged(editorState), pageID(), IPC::DispatchMessageEvenWhenWaitingForSyncReply);
 #endif
 
 #if PLATFORM(IOS)
@@ -4385,6 +4395,15 @@ void WebPage::didChangeSelection()
 #endif
 }
 
+void WebPage::sendPostLayoutEditorStateIfNeeded()
+{
+    if (!m_isEditorStateMissingPostLayoutData)
+        return;
+
+    send(Messages::WebPageProxy::EditorStateChanged(editorState(IncludePostLayoutDataHint::Yes)), pageID(), IPC::DispatchMessageEvenWhenWaitingForSyncReply);
+    m_isEditorStateMissingPostLayoutData = false;
+}
+
 void WebPage::discardedComposition()
 {
     send(Messages::WebPageProxy::CompositionWasCanceled(editorState()));
index 5302b7d1418157bfc0125ea33b524e7d7e363090..963a87372063d981b73626c8346a858e92b4a0cf 100644 (file)
@@ -327,7 +327,9 @@ public:
     WebCore::WebGLLoadPolicy resolveWebGLPolicyForURL(WebFrame*, const String&);
 #endif // ENABLE(WEBGL)
     
-    EditorState editorState() const;
+    enum class IncludePostLayoutDataHint { No, Yes };
+    EditorState editorState(IncludePostLayoutDataHint = IncludePostLayoutDataHint::Yes) const;
+    void sendPostLayoutEditorStateIfNeeded();
 
     String renderTreeExternalRepresentation() const;
     String renderTreeExternalRepresentationForPrinting() const;
@@ -878,7 +880,7 @@ private:
 
     void platformInitialize();
     void platformDetach();
-    void platformEditorState(WebCore::Frame&, EditorState& result) const;
+    void platformEditorState(WebCore::Frame&, EditorState& result, IncludePostLayoutDataHint) const;
 
     void didReceiveWebPageMessage(IPC::Connection&, IPC::MessageDecoder&);
     void didReceiveSyncWebPageMessage(IPC::Connection&, IPC::MessageDecoder&, std::unique_ptr<IPC::MessageEncoder>&);
@@ -1353,6 +1355,7 @@ private:
 
     bool m_mainFrameProgressCompleted;
     bool m_shouldDispatchFakeMouseMoveEvents;
+    bool m_isEditorStateMissingPostLayoutData { false };
 };
 
 } // namespace WebKit
index 33540179290c230846a796bf84ada2f6977adf28..c231b6249942f5f9a3f92a63bf39d5fc736764d0 100644 (file)
@@ -82,7 +82,7 @@ void WebPage::platformPreferencesDidChange(const WebPreferencesStore&)
     notImplemented();
 }
 
-void WebPage::platformEditorState(Frame&, EditorState&) const
+void WebPage::platformEditorState(Frame&, EditorState&, IncludePostLayoutDataHint) const
 {
 }
 
index 2d0324a517039011d38f3e1997f464706fe235d0..3f2ab945bba9d0638c1c93bf9aca4bd7a67b9f15 100644 (file)
@@ -68,7 +68,7 @@ void WebPage::platformDetach()
 {
 }
 
-void WebPage::platformEditorState(Frame& frame, EditorState& result) const
+void WebPage::platformEditorState(Frame& frame, EditorState& result, IncludePostLayoutDataHint) const
 {
     result.cursorRect = frame.selection().absoluteCaretBounds();
 }
index a0d9436afce93986b532e9374c3af75d3ad27b99..b7ba137fee4f5cb136942a00e3685ead5e055c7d 100644 (file)
@@ -128,9 +128,8 @@ void WebPage::platformPreferencesDidChange(const WebPreferencesStore&)
     notImplemented();
 }
 
-void WebPage::platformEditorState(Frame& frame, EditorState& result) const
+void WebPage::platformEditorState(Frame& frame, EditorState& result, IncludePostLayoutDataHint shouldIncludePostLayoutData) const
 {
-    const VisibleSelection& selection = frame.selection().selection();
     if (frame.editor().hasComposition()) {
         RefPtr<Range> compositionRange = frame.editor().compositionRange();
         Vector<WebCore::SelectionRect> compositionRects;
@@ -145,34 +144,46 @@ void WebPage::platformEditorState(Frame& frame, EditorState& result) const
             result.markedText = plainTextReplacingNoBreakSpace(compositionRange.get());
         }
     }
+
+    // We only set the remaining EditorState entries if the layout is done. To compute these
+    // entries, we need the layout to be done and we don't want to trigger a synchronous
+    // layout as this would be bad for performance. If we have a composition, we send everything
+    // right away as the UIProcess needs the caretRects ASAP for marked text.
+    if (shouldIncludePostLayoutData == IncludePostLayoutDataHint::No && !frame.editor().hasComposition()) {
+        result.isMissingPostLayoutData = true;
+        return;
+    }
+
+    auto& postLayoutData = result.postLayoutData();
     FrameView* view = frame.view();
+    const VisibleSelection& selection = frame.selection().selection();
     if (selection.isCaret()) {
-        result.caretRectAtStart = view->contentsToRootView(frame.selection().absoluteCaretBounds());
-        result.caretRectAtEnd = result.caretRectAtStart;
+        postLayoutData.caretRectAtStart = view->contentsToRootView(frame.selection().absoluteCaretBounds());
+        postLayoutData.caretRectAtEnd = postLayoutData.caretRectAtStart;
         // FIXME: The following check should take into account writing direction.
-        result.isReplaceAllowed = result.isContentEditable && atBoundaryOfGranularity(selection.start(), WordGranularity, DirectionForward);
-        result.wordAtSelection = plainTextReplacingNoBreakSpace(wordRangeFromPosition(selection.start()).get());
+        postLayoutData.isReplaceAllowed = result.isContentEditable && atBoundaryOfGranularity(selection.start(), WordGranularity, DirectionForward);
+        postLayoutData.wordAtSelection = plainTextReplacingNoBreakSpace(wordRangeFromPosition(selection.start()).get());
         if (selection.isContentEditable()) {
-            charactersAroundPosition(selection.start(), result.characterAfterSelection, result.characterBeforeSelection, result.twoCharacterBeforeSelection);
+            charactersAroundPosition(selection.start(), postLayoutData.characterAfterSelection, postLayoutData.characterBeforeSelection, postLayoutData.twoCharacterBeforeSelection);
             Node* root = selection.rootEditableElement();
-            result.hasContent = root && root->hasChildNodes() && !isEndOfEditableOrNonEditableContent(firstPositionInNode(root));
+            postLayoutData.hasContent = root && root->hasChildNodes() && !isEndOfEditableOrNonEditableContent(firstPositionInNode(root));
         }
     } else if (selection.isRange()) {
-        result.caretRectAtStart = view->contentsToRootView(VisiblePosition(selection.start()).absoluteCaretBounds());
-        result.caretRectAtEnd = view->contentsToRootView(VisiblePosition(selection.end()).absoluteCaretBounds());
+        postLayoutData.caretRectAtStart = view->contentsToRootView(VisiblePosition(selection.start()).absoluteCaretBounds());
+        postLayoutData.caretRectAtEnd = view->contentsToRootView(VisiblePosition(selection.end()).absoluteCaretBounds());
         RefPtr<Range> selectedRange = selection.toNormalizedRange();
         String selectedText;
         if (selectedRange) {
-            selectedRange->collectSelectionRects(result.selectionRects);
-            convertSelectionRectsToRootView(view, result.selectionRects);
+            selectedRange->collectSelectionRects(postLayoutData.selectionRects);
+            convertSelectionRectsToRootView(view, postLayoutData.selectionRects);
             selectedText = plainTextReplacingNoBreakSpace(selectedRange.get(), TextIteratorDefaultBehavior, true);
-            result.selectedTextLength = selectedText.length();
+            postLayoutData.selectedTextLength = selectedText.length();
             const int maxSelectedTextLength = 200;
             if (selectedText.length() <= maxSelectedTextLength)
-                result.wordAtSelection = selectedText;
+                postLayoutData.wordAtSelection = selectedText;
         }
         // FIXME: We should disallow replace when the string contains only CJ characters.
-        result.isReplaceAllowed = result.isContentEditable && !result.isInPasswordField && !selectedText.containsOnlyWhitespace();
+        postLayoutData.isReplaceAllowed = result.isContentEditable && !result.isInPasswordField && !selectedText.containsOnlyWhitespace();
     }
     if (!selection.isNone()) {
         Node* nodeToRemove;
@@ -181,18 +192,18 @@ void WebPage::platformEditorState(Frame& frame, EditorState& result) const
             CTFontSymbolicTraits traits = font ? CTFontGetSymbolicTraits(font) : 0;
             
             if (traits & kCTFontTraitBold)
-                result.typingAttributes |= AttributeBold;
+                postLayoutData.typingAttributes |= AttributeBold;
             if (traits & kCTFontTraitItalic)
-                result.typingAttributes |= AttributeItalics;
+                postLayoutData.typingAttributes |= AttributeItalics;
             
             RefPtr<EditingStyle> typingStyle = frame.selection().typingStyle();
             if (typingStyle && typingStyle->style()) {
                 String value = typingStyle->style()->getPropertyValue(CSSPropertyWebkitTextDecorationsInEffect);
                 if (value.contains("underline"))
-                    result.typingAttributes |= AttributeUnderline;
+                    postLayoutData.typingAttributes |= AttributeUnderline;
             } else {
                 if (style->textDecorationsInEffect() & TextDecorationUnderline)
-                    result.typingAttributes |= AttributeUnderline;
+                    postLayoutData.typingAttributes |= AttributeUnderline;
             }
             
             if (nodeToRemove)
index 302ac7b800cfb572b254abd6b611f90fec480a08..d8d71f600755e56a0469f5a05150f958c6d4ca29 100644 (file)
@@ -122,7 +122,7 @@ void WebPage::platformDetach()
     [m_mockAccessibilityElement setWebPage:nullptr];
 }
 
-void WebPage::platformEditorState(Frame& frame, EditorState& result) const
+void WebPage::platformEditorState(Frame& frame, EditorState& result, IncludePostLayoutDataHint) const
 {
 }