[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 c278a06..7ef58b5 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 a8f1b41..e39bfc6 100644 (file)
@@ -381,6 +381,8 @@ void FrameSelection::updateAndRevealSelection()
     }
 
     notifyAccessibilityForSelectionChange();
+
+    m_frame->editor().client()->didChangeSelectionAndUpdateLayout();
 }
 
 void FrameSelection::updateDataDetectorsForSelection()
index ce8c240..9fd11dd 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 e18c027..2d551c7 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 66cf723..d550198 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 90e166a..6478038 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 2b11849..a55f0d5 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 4813761..15a0027 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 4ae7ef3..286d15d 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 9bc0b8f..c15ff7a 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 979ad5b..4732066 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 c15af20..fc0ce44 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 8f1d755..da72dec 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 34afbba..b8cbcce 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 5fc6131..7705519 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 9716263..46d38e5 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 5302b7d..963a873 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 3354017..c231b62 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 2d0324a..3f2ab94 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 a0d9436..b7ba137 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 302ac7b..d8d71f6 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
 {
 }