https://bugs.webkit.org/show_bug.cgi?id=175370
<rdar://problem/
33799806>
Reviewed by Ryosuke Niwa.
Source/WebCore:
Remove didChangeSelectionAndUpdateLayout -- EditorState updates that are scheduled due to missing post-layout
data will now be scheduled for the next presentation update. Additionally, add editor client hooks to notify the
WebKit layer when we've updated the current composition. See WebKit ChangeLog for more details. This patch
adjusts and rebaselines existing layout tests.
* editing/Editor.cpp:
(WebCore::SetCompositionScope::SetCompositionScope):
(WebCore::SetCompositionScope::~SetCompositionScope):
Introduce a helper RAII class to ensure that we ignore selection changes during the scope of
Editor::setComposition and call out to the client with WebEditorClient::didUpdateComposition afterwards. This
also maintains a UserTypingGestureIndicator over its lifetime, so we don't additionally need to create a
UserTypingGestureIndicator in Editor::setComposition.
(WebCore::Editor::setComposition):
* editing/FrameSelection.cpp:
(WebCore::FrameSelection::setSelection):
(WebCore::FrameSelection::updateAndRevealSelection):
(WebCore::FrameSelection::setSelectedRange):
* editing/FrameSelection.h:
(WebCore::FrameSelection::defaultSetSelectionOptions):
Plumb state about whether or not the selection change was triggered by the user to FrameSelection::setSelection,
and if so, notify the editing client. A separate SetSelectionOptions flag is used here instead of
RevealSelection to avoid calling out to the client in places where we want to reveal the selection regardless of
whether or not the selection is user triggered.
* loader/EmptyClients.cpp:
* page/EditorClient.h:
Source/WebKit:
See per-method comments for more detail. WebPage::didChangeSelection now schedules EditorState updates to be sent
during the next layer tree transaction rather than sending them synchronously. To ensure that iOS and Mac continue
to behave correctly w.r.t. EditorState updates, we immediately dispatch EditorStates in the following cases:
- After the composition changes, is confirmed, or is canceled.
- After an edit command is executed.
- After ending user-triggered selection changes.
* Shared/mac/RemoteLayerTreeTransaction.h:
(WebKit::RemoteLayerTreeTransaction::hasEditorState const):
(WebKit::RemoteLayerTreeTransaction::editorState const):
(WebKit::RemoteLayerTreeTransaction::setEditorState):
Attaches an optional EditorState to the RemoteLayerTreeTransaction. This EditorState is computed and sent over
when setting up the transaction in WebPage, if something previously scheduled an EditorState update.
* Shared/mac/RemoteLayerTreeTransaction.mm:
(WebKit::RemoteLayerTreeTransaction::encode const):
(WebKit::RemoteLayerTreeTransaction::decode):
Add coder support for sending over a layer tree transaction's EditorState.
* UIProcess/API/Cocoa/WKViewPrivate.h:
* UIProcess/API/mac/WKView.mm:
(-[WKView _doAfterNextPresentationUpdate:]):
Add _doAfterNextPresentationUpdate to WKView (used in TestWebKitAPI -- refer to
WebKitAgnosticTest::waitForNextPresentationUpdate).
* UIProcess/DrawingAreaProxy.h:
(WebKit::DrawingAreaProxy::dispatchPresentationCallbacksAfterFlushingLayers):
* UIProcess/DrawingAreaProxy.messages.in:
Add a new IPC messages, DispatchPresentationCallbacksAfterFlushingLayers, to invoke in-flight presentation
callbacks in the UI process following a layer flush in the web process.
* UIProcess/WebPageProxy.h:
* UIProcess/mac/RemoteLayerTreeDrawingAreaProxy.mm:
(WebKit::RemoteLayerTreeDrawingAreaProxy::commitLayerTree):
* UIProcess/mac/TiledCoreAnimationDrawingAreaProxy.h:
* UIProcess/mac/TiledCoreAnimationDrawingAreaProxy.mm:
(WebKit::TiledCoreAnimationDrawingAreaProxy::~TiledCoreAnimationDrawingAreaProxy):
(WebKit::TiledCoreAnimationDrawingAreaProxy::dispatchAfterEnsuringDrawing):
(WebKit::TiledCoreAnimationDrawingAreaProxy::dispatchPresentationCallbacksAfterFlushingLayers):
Run all pending _doAfterNextPresentationUpdate callbacks.
* WebProcess/WebCoreSupport/WebEditorClient.cpp:
(WebKit::WebEditorClient::didApplyStyle):
(WebKit::WebEditorClient::respondToChangedContents):
(WebKit::WebEditorClient::didEndUserTriggeredSelectionChanges):
(WebKit::WebEditorClient::didUpdateComposition):
Forward editor client calls to the WebPage.
(WebKit::WebEditorClient::didChangeSelectionAndUpdateLayout): Deleted.
* WebProcess/WebCoreSupport/WebEditorClient.h:
* WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::editorState const):
(WebKit::WebPage::updateEditorStateAfterLayoutIfEditabilityChanged):
(WebKit::WebPage::willCommitLayerTree):
(WebKit::WebPage::didApplyStyle):
Allow style application to immediately trigger EditorState updates, if we're not currently ignoring selection
changes in the Editor.
(WebKit::WebPage::didChangeContents):
Allow applying top-level edit commands to immediately trigger EditorState updates, if we're not currently
ignoring selection changes in the Editor.
(WebKit::WebPage::didChangeSelection):
(WebKit::WebPage::didUpdateComposition):
(WebKit::WebPage::didEndUserTriggeredSelectionChanges):
(WebKit::WebPage::discardedComposition):
(WebKit::WebPage::canceledComposition):
When handling composition updates, always send an EditorState to the UI process. Unlike other cases, IME
requires immediate EditorState data, so we need to be explicit here in sending updates right away.
(WebKit::WebPage::sendEditorStateUpdate):
(WebKit::WebPage::sendPartialEditorStateAndSchedulePostLayoutUpdate):
(WebKit::WebPage::flushPendingEditorStateUpdate):
Helper methods to schedule an EditorState update to be sent upon the next layer tree update, or flush any
pending EditorState update that has been scheduled. The private, more aggressive variant of this is
sendEditorStateUpdate, which ignores whether or not there was already an EditorState update scheduled, and sends
one anyways (this still fulfills any EditorState update that was previously scheduled).
These helper methods are treated as no-ops when invoked while ignoring selection changes. This is to prevent
temporary selection state and editor commands during operations such as text indicator snapshotting from pushing
bogus information about transient editor states to the UI process.
(WebKit::WebPage::sendPostLayoutEditorStateIfNeeded): Deleted.
* WebProcess/WebPage/WebPage.h:
* WebProcess/WebPage/ios/WebPageIOS.mm:
(WebKit::WebPage::platformEditorState const):
(WebKit::WebPage::executeEditCommandWithCallback):
(WebKit::selectionIsInsideFixedPositionContainer):
(WebKit::WebPage::updateVisibleContentRects):
Fix a hack that was computing an EditorState to figure out whether the current selection starts or ends in a
fixed position container. Factors out relevant logic into a separate helper, and also schedules an EditorState
update instead of immediately computing it.
* WebProcess/WebPage/mac/TiledCoreAnimationDrawingArea.h:
* WebProcess/WebPage/mac/TiledCoreAnimationDrawingArea.mm:
(WebKit::TiledCoreAnimationDrawingArea::addTransactionCallbackID):
Add support for registering and dispatching presentation callbacks that hook into the layer flush lifecycle,
using the tiled CA drawing area. These are used by Mac LayoutTests and API tests that need to wait until the
next flush before checking for state that depends on EditorState updates in the UI process.
(WebKit::TiledCoreAnimationDrawingArea::flushLayers):
Tell the WebPage to flush any pending EditorState updates.
* WebProcess/WebPage/mac/WebPageMac.mm:
(WebKit::WebPage::platformEditorState const):
Source/WebKitLegacy/mac:
Adjust WebEditorClient for interface changes.
* WebCoreSupport/WebEditorClient.h:
Source/WebKitLegacy/win:
Adjust WebEditorClient for interface changes.
* WebCoreSupport/WebEditorClient.h:
Tools:
Tweaks API tests that involve editing to wait for a presentation update before checking against UI process-side
information sent via EditorState updates. This allows any EditorState update scheduled by the test to propagate
to the UI process.
* TestWebKitAPI/Tests/WebKit2Cocoa/WKWebViewCandidateTests.mm:
(-[CandidateTestWebView typeString:inputMessage:]):
(+[CandidateTestWebView setUpWithFrame:testPage:]):
* TestWebKitAPI/Tests/WebKit2Cocoa/WKWebViewTextInput.mm:
* TestWebKitAPI/Tests/mac/AcceptsFirstMouse.mm:
(TestWebKitAPI::AcceptsFirstMouse::runTest):
* TestWebKitAPI/Tests/mac/WKWebViewMacEditingTests.mm:
* TestWebKitAPI/cocoa/TestWKWebView.h:
* TestWebKitAPI/cocoa/TestWKWebView.mm:
(-[TestWKWebView waitForNextPresentationUpdate]):
Add a new helper method to spin until the next presentation update.
* TestWebKitAPI/mac/WebKitAgnosticTest.h:
* TestWebKitAPI/mac/WebKitAgnosticTest.mm:
(TestWebKitAPI::WebKitAgnosticTest::waitForNextPresentationUpdate):
LayoutTests:
Rebaseline and adjust LayoutTests.
* editing/caret/ios/absolute-caret-position-after-scroll-expected.txt:
* editing/caret/ios/absolute-caret-position-after-scroll.html:
* editing/caret/ios/fixed-caret-position-after-scroll-expected.txt:
* editing/caret/ios/fixed-caret-position-after-scroll.html:
* editing/secure-input/password-input-changed-type.html:
* editing/secure-input/password-input-focusing.html:
* editing/secure-input/removed-password-input.html:
* editing/secure-input/reset-state-on-navigation.html:
* editing/selection/character-granularity-rect.html:
Delay checking for secure input state and caret rects until after the next presentation update.
* editing/selection/ios/absolute-selection-after-scroll-expected.txt:
* editing/selection/ios/absolute-selection-after-scroll.html:
* editing/selection/ios/fixed-selection-after-scroll-expected.txt:
* editing/selection/ios/fixed-selection-after-scroll.html:
Refactor and simplify these tests. These tests are not run on the OpenSource bots, since they depend on long
press and tap gestures.
* platform/ios-wk2/editing/inserting/insert-div-024-expected.txt:
* platform/ios-wk2/editing/inserting/insert-div-026-expected.txt:
* platform/ios-wk2/editing/style/
5084241-expected.txt:
Rebaselines these tests, removing an anonymous RenderBlock inserted as a result of inserting and removing a
dummy span in order to compute a RenderStyle in WebPage::editorState. This is because editorState is no longer
invoked immediately on page load; https://bugs.webkit.org/show_bug.cgi?id=175116 tracks preventing this render
tree thrashing altogether.
* platform/mac-wk2/TestExpectations:
* platform/mac-wk2/editing/style/unbold-in-bold-expected.txt:
* resources/ui-helper.js:
Introduce new UIHelper functions.
(window.UIHelper.ensurePresentationUpdate.return.new.Promise):
(window.UIHelper.ensurePresentationUpdate):
Returns a Promise, resolved after the next presentation update.
(window.UIHelper.activateAndWaitForInputSessionAt.return.new.Promise.):
(window.UIHelper.activateAndWaitForInputSessionAt.return.new.Promise):
(window.UIHelper.activateAndWaitForInputSessionAt):
Returns a Promise, resolved after tapping at the given location and waiting for the keyboard to appear on iOS.
(window.UIHelper.getUICaretRect.return.new.Promise.):
(window.UIHelper.getUICaretRect.return.new.Promise):
(window.UIHelper.getUICaretRect):
(window.UIHelper.getUISelectionRects.return.new.Promise.):
(window.UIHelper.getUISelectionRects.return.new.Promise):
(window.UIHelper.getUISelectionRects):
Helpers to fetch selection and caret rect information in the UI process.
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@221064
268f45cc-cd09-0410-ab3c-
d52691b4dbfc
+2017-08-22 Wenson Hsieh <wenson_hsieh@apple.com>
+
+ [WK2] EditorState updates should be rolled into the layer update lifecycle when possible
+ https://bugs.webkit.org/show_bug.cgi?id=175370
+ <rdar://problem/33799806>
+
+ Reviewed by Ryosuke Niwa.
+
+ Rebaseline and adjust LayoutTests.
+
+ * editing/caret/ios/absolute-caret-position-after-scroll-expected.txt:
+ * editing/caret/ios/absolute-caret-position-after-scroll.html:
+ * editing/caret/ios/fixed-caret-position-after-scroll-expected.txt:
+ * editing/caret/ios/fixed-caret-position-after-scroll.html:
+ * editing/secure-input/password-input-changed-type.html:
+ * editing/secure-input/password-input-focusing.html:
+ * editing/secure-input/removed-password-input.html:
+ * editing/secure-input/reset-state-on-navigation.html:
+ * editing/selection/character-granularity-rect.html:
+
+ Delay checking for secure input state and caret rects until after the next presentation update.
+
+ * editing/selection/ios/absolute-selection-after-scroll-expected.txt:
+ * editing/selection/ios/absolute-selection-after-scroll.html:
+ * editing/selection/ios/fixed-selection-after-scroll-expected.txt:
+ * editing/selection/ios/fixed-selection-after-scroll.html:
+
+ Refactor and simplify these tests. These tests are not run on the OpenSource bots, since they depend on long
+ press and tap gestures.
+
+ * platform/ios-wk2/editing/inserting/insert-div-024-expected.txt:
+ * platform/ios-wk2/editing/inserting/insert-div-026-expected.txt:
+ * platform/ios-wk2/editing/style/5084241-expected.txt:
+
+ Rebaselines these tests, removing an anonymous RenderBlock inserted as a result of inserting and removing a
+ dummy span in order to compute a RenderStyle in WebPage::editorState. This is because editorState is no longer
+ invoked immediately on page load; https://bugs.webkit.org/show_bug.cgi?id=175116 tracks preventing this render
+ tree thrashing altogether.
+
+ * platform/mac-wk2/TestExpectations:
+ * platform/mac-wk2/editing/style/unbold-in-bold-expected.txt:
+ * resources/ui-helper.js:
+
+ Introduce new UIHelper functions.
+
+ (window.UIHelper.ensurePresentationUpdate.return.new.Promise):
+ (window.UIHelper.ensurePresentationUpdate):
+
+ Returns a Promise, resolved after the next presentation update.
+
+ (window.UIHelper.activateAndWaitForInputSessionAt.return.new.Promise.):
+ (window.UIHelper.activateAndWaitForInputSessionAt.return.new.Promise):
+ (window.UIHelper.activateAndWaitForInputSessionAt):
+
+ Returns a Promise, resolved after tapping at the given location and waiting for the keyboard to appear on iOS.
+
+ (window.UIHelper.getUICaretRect.return.new.Promise.):
+ (window.UIHelper.getUICaretRect.return.new.Promise):
+ (window.UIHelper.getUICaretRect):
+ (window.UIHelper.getUISelectionRects.return.new.Promise.):
+ (window.UIHelper.getUISelectionRects.return.new.Promise):
+ (window.UIHelper.getUISelectionRects):
+
+ Helpers to fetch selection and caret rect information in the UI process.
+
2017-08-21 Ryosuke Niwa <rniwa@webkit.org>
Consolidate the code to normalize MIME type in DataTransfer
+PASS initialCaretRect.left is 6
+PASS initialCaretRect.top is 21
+PASS initialCaretRect.width is 3
+PASS initialCaretRect.height is 15
+PASS finalCaretRect.left is 6
+PASS finalCaretRect.top is 21
+PASS finalCaretRect.width is 3
+PASS finalCaretRect.height is 15
PASS successfullyParsed is true
TEST COMPLETE
-The initial caret rect is: [6 21 ; 3 15]
-The caret rect after scrolling 1000px down is: [6 21 ; 3 15]
-PASS finalCaretRect.top is initialCaretRect.top
-PASS finalCaretRect.left is initialCaretRect.left
-PASS finalCaretRect.width is initialCaretRect.width
-PASS finalCaretRect.height is initialCaretRect.height
<html>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<head>
+ <script src="../../../resources/ui-helper.js"></script>
<script src="../../../resources/js-test.js"></script>
<style>
body {
div {
background-image: linear-gradient(0deg, blue, red);
- height: 4000px;
+ height: 10000px;
}
</style>
- <script>
- jsTestIsAsync = true;
-
- function tapInInputScript(tapX, tapY)
- {
- return `(function() {
- uiController.didShowKeyboardCallback = function() {
- uiController.doAfterNextStablePresentationUpdate(function() {
- uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionCaretRect));
- });
- };
- uiController.singleTapAtPoint(${tapX}, ${tapY}, function() { });
- })()`;
- }
-
- function simulateScrollingScript(distance)
- {
- return `(function() {
- uiController.stableStateOverride = false;
- uiController.immediateScrollToOffset(0, ${distance});
- uiController.stableStateOverride = true;
- uiController.doAfterNextStablePresentationUpdate(function() {
- uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionCaretRect));
- });
- })()`;
- }
-
- function toString(rect)
- {
- return `[${rect.left} ${rect.top} ; ${rect.width} ${rect.height}]`;
- }
-
- function run()
- {
- if (!window.testRunner || !testRunner.runUIScript) {
- description("To manually test, tap this input field and scroll up. The text caret should not end up outside of the input.");
- return;
- }
-
- testRunner.runUIScript(tapInInputScript(window.innerWidth / 2, 30), initialCaretRect => {
- initialCaretRect = JSON.parse(initialCaretRect);
- window.initialCaretRect = initialCaretRect;
- debug(`The initial caret rect is: ${toString(initialCaretRect)}`);
- testRunner.runUIScript(simulateScrollingScript(1000), finalCaretRect => {
- finalCaretRect = JSON.parse(finalCaretRect);
- window.finalCaretRect = finalCaretRect;
- debug(`The caret rect after scrolling 1000px down is: ${toString(finalCaretRect)}`);
- shouldBe("finalCaretRect.top", "initialCaretRect.top");
- shouldBe("finalCaretRect.left", "initialCaretRect.left");
- shouldBe("finalCaretRect.width", "initialCaretRect.width");
- shouldBe("finalCaretRect.height", "initialCaretRect.height");
- finishJSTest();
- });
- });
- }
- </script>
</head>
-<body onload=run()>
- <input></input>
+<body>
+<div>
+<input id="input"></input>
+</div>
</body>
+<script>
+jsTestIsAsync = true;
+
+(() => {
+ if (!window.testRunner || !testRunner.runUIScript) {
+ description("To manually test, tap this input field and scroll up. The text caret should not end up outside of the input.");
+ return;
+ }
+ UIHelper.activateAndWaitForInputSessionAt(innerWidth / 2, 30)
+ .then(() => UIHelper.getUICaretRect())
+ .then((rect) => {
+ initialCaretRect = rect;
+ shouldBe("initialCaretRect.left", "6");
+ shouldBe("initialCaretRect.top", "21");
+ shouldBe("initialCaretRect.width", "3");
+ shouldBe("initialCaretRect.height", "15");
+ document.body.scrollTop += 5000;
+ return UIHelper.getUICaretRect();
+ })
+ .then((rect) => {
+ finalCaretRect = rect;
+ shouldBe("finalCaretRect.left", "6");
+ shouldBe("finalCaretRect.top", "21");
+ shouldBe("finalCaretRect.width", "3");
+ shouldBe("finalCaretRect.height", "15");
+ finishJSTest();
+ });
+})();
+</script>
</html>
+PASS initialCaretRect.left is 6
+PASS initialCaretRect.top is 21
+PASS initialCaretRect.width is 3
+PASS initialCaretRect.height is 15
+PASS finalCaretRect.left is 6
+PASS finalCaretRect.top is 5021
+PASS finalCaretRect.width is 3
+PASS finalCaretRect.height is 15
PASS successfullyParsed is true
TEST COMPLETE
-PASS finalCaretRect.top - initialCaretRect.top is 500
<html>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<head>
+ <script src="../../../resources/ui-helper.js"></script>
<script src="../../../resources/js-test.js"></script>
<style>
body {
div {
background-image: linear-gradient(blue, red);
- height: 4000px;
+ height: 10000px;
}
</style>
- <script>
- jsTestIsAsync = true;
-
- function getInputViewBoundsAfterTappingInInputScript(tapX, tapY)
- {
- return `(function() {
- uiController.didShowKeyboardCallback = function() {
- uiController.doAfterNextStablePresentationUpdate(function() {
- uiController.uiScriptComplete(JSON.stringify(uiController.inputViewBounds));
- });
- };
- uiController.singleTapAtPoint(${tapX}, ${tapY}, function() { });
- })()`;
- }
-
- function getCaretRectAfterScrollToOffsetScript(distance)
- {
- return `(function() {
- uiController.stableStateOverride = false;
- uiController.immediateScrollToOffset(0, ${distance});
- uiController.stableStateOverride = true;
- uiController.doAfterNextStablePresentationUpdate(function() {
- uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionCaretRect));
- });
- })()`;
- }
-
- function toString(rect)
- {
- return `[${rect.left} ${rect.top} ; ${rect.width} ${rect.height}]`;
- }
-
- function run()
- {
- if (!window.testRunner || !testRunner.runUIScript) {
- description("To manually test, tap this input field and scroll up. The text caret should not end up outside of the input.");
- return;
- }
-
- testRunner.runUIScript(getInputViewBoundsAfterTappingInInputScript(window.innerWidth / 2, 30), (inputViewBounds) => {
- window.inputViewBounds = inputViewBounds = JSON.parse(inputViewBounds);
- testRunner.runUIScript(getCaretRectAfterScrollToOffsetScript(inputViewBounds.height), initialCaretRect => {
- window.initialCaretRect = initialCaretRect = JSON.parse(initialCaretRect);
- testRunner.runUIScript(getCaretRectAfterScrollToOffsetScript(inputViewBounds.height + 500), finalCaretRect => {
- window.finalCaretRect = finalCaretRect = JSON.parse(finalCaretRect);
- shouldBe("finalCaretRect.top - initialCaretRect.top", "500");
- finishJSTest();
- });
- });
- });
- }
- </script>
</head>
-<body onload=run()>
- <input id="input"></input>
+<body>
+<div>
+<input id="input"></input>
+</div>
</body>
+<script>
+jsTestIsAsync = true;
+
+(() => {
+ if (!window.testRunner || !testRunner.runUIScript) {
+ description("To manually test, tap this input field and scroll up. The text caret should not end up outside of the input.");
+ return;
+ }
+ UIHelper.activateAndWaitForInputSessionAt(innerWidth / 2, 30)
+ .then(() => UIHelper.getUICaretRect())
+ .then((rect) => {
+ initialCaretRect = rect;
+ shouldBe("initialCaretRect.left", "6");
+ shouldBe("initialCaretRect.top", "21");
+ shouldBe("initialCaretRect.width", "3");
+ shouldBe("initialCaretRect.height", "15");
+ document.body.scrollTop += 5000;
+ return UIHelper.getUICaretRect();
+ })
+ .then((rect) => {
+ finalCaretRect = rect;
+ shouldBe("finalCaretRect.left", "6");
+ shouldBe("finalCaretRect.top", "5021");
+ shouldBe("finalCaretRect.width", "3");
+ shouldBe("finalCaretRect.height", "15");
+ finishJSTest();
+ });
+})();
+</script>
</html>
<html>
<head>
<meta charset="utf-8">
+<script src="../../resources/ui-helper.js"></script>
<script src="../../resources/js-test-pre.js"></script>
</head>
<body>
<input type=password>
<script>
+jsTestIsAsync = true;
+
description("Verify that changing a password input's type updates secure input state.");
var passwordInput = document.getElementsByTagName("input")[0];
debug("A password input is focused:");
passwordInput.focus();
-shouldBe("testRunner.secureEventInputIsEnabled", "true");
-debug("\nAfter changing the type to text:");
-passwordInput.type = "text";
-shouldBe("testRunner.secureEventInputIsEnabled", "false");
+UIHelper.ensurePresentationUpdate().then(() => {
+ shouldBe("testRunner.secureEventInputIsEnabled", "true");
+ debug("\nAfter changing the type to text:");
+ passwordInput.type = "text";
+})
+
+.then(() => UIHelper.ensurePresentationUpdate())
+.then(() => {
+ shouldBe("testRunner.secureEventInputIsEnabled", "false");
+ debug("\nAfter changing the type back to password:");
+ passwordInput.type = "password";
+})
-debug("\nAfter changing the type back to password:");
-passwordInput.type = "password";
-shouldBe("testRunner.secureEventInputIsEnabled", "true");
+.then(() => UIHelper.ensurePresentationUpdate())
+.then(() => {
+ shouldBe("testRunner.secureEventInputIsEnabled", "true");
+ finishJSTest();
+});
</script>
<script src="../../resources/js-test-post.js"></script>
<html>
<head>
<meta charset="utf-8">
+<script src="../../resources/ui-helper.js"></script>
<script src="../../resources/js-test-pre.js"></script>
</head>
<body>
<input type=password>
<input type=text>
<script>
+jsTestIsAsync = true;
description("Verify that basic focusing/unfocusing updates secure input state.");
debug("\nA password input is focused:");
passwordInput.focus();
-shouldBe("testRunner.secureEventInputIsEnabled", "true");
-debug("\nA regular text input is focused:");
-textInput.focus();
-shouldBe("testRunner.secureEventInputIsEnabled", "false");
+UIHelper.ensurePresentationUpdate().then(() => {
+ shouldBe("testRunner.secureEventInputIsEnabled", "true");
+ debug("\nA regular text input is focused:");
+ textInput.focus();
+})
-debug("\nA password input is focused again:");
-passwordInput.focus();
-shouldBe("testRunner.secureEventInputIsEnabled", "true");
+.then(() => UIHelper.ensurePresentationUpdate())
+.then(() => {
+ shouldBe("testRunner.secureEventInputIsEnabled", "false");
+ debug("\nA password input is focused again:");
+ passwordInput.focus();
+})
+.then(() => UIHelper.ensurePresentationUpdate())
+.then(() => {
+ shouldBe("testRunner.secureEventInputIsEnabled", "true");
+ finishJSTest();
+});
</script>
<script src="../../resources/js-test-post.js"></script>
</body>
<html>
<head>
<meta charset="utf-8">
+<script src="../../resources/ui-helper.js"></script>
<script src="../../resources/js-test-pre.js"></script>
</head>
<body>
<input type=password>
<script>
+jsTestIsAsync = true;
+
description("Verify that removing a password input from DOM tree disables secure input state.");
var passwordInput = document.getElementsByTagName("input")[0];
debug("A password input is focused:");
passwordInput.focus();
-shouldBe("testRunner.secureEventInputIsEnabled", "true");
-debug("\nAfter deleting the input:");
-document.body.removeChild(passwordInput);
-shouldBe("testRunner.secureEventInputIsEnabled", "false");
+UIHelper.ensurePresentationUpdate().then(() => {
+ shouldBe("testRunner.secureEventInputIsEnabled", "true");
+ debug("\nAfter deleting the input:");
+ document.body.removeChild(passwordInput);
+})
+
+.then(() => {
+ return UIHelper.ensurePresentationUpdate()
+})
+
+.then(() => {
+ shouldBe("testRunner.secureEventInputIsEnabled", "false");
+ finishJSTest();
+})
</script>
<script src="../../resources/js-test-post.js"></script>
<!DOCTYPE html>
<html>
+<head>
+<script src="../../resources/ui-helper.js"></script>
+</head>
<body>
<input type=password>
<script>
var passwordInput = document.getElementsByTagName("input")[0];
passwordInput.focus();
-if (!testRunner.secureEventInputIsEnabled)
- alert("FAIL: Secure event input is not enabled after focusing a password input");
+UIHelper.ensurePresentationUpdate().then(() => {
+ if (!testRunner.secureEventInputIsEnabled)
+ alert("FAIL: Secure event input is not enabled after focusing a password input");
-location = "resources/reset-state-on-navigation-target.html";
+ location = "resources/reset-state-on-navigation-target.html";
+})
</script>
</body>
+PASS initialSelectionRects.length is 1
+PASS initialSelectionRects[0].left is 0
+PASS initialSelectionRects[0].top is 0
+PASS initialSelectionRects[0].width is 309
+PASS initialSelectionRects[0].height is 114
+PASS finalSelectionRects.length is 1
+PASS finalSelectionRects[0].left is 0
+PASS finalSelectionRects[0].top is 0
+PASS finalSelectionRects[0].width is 309
+PASS finalSelectionRects[0].height is 114
PASS successfullyParsed is true
TEST COMPLETE
-After long pressing, the selection rects are: [0 0 ; 309 114]
-After scrolling 1000px down, the selection rects are: [0 0 ; 309 114]
-PASS finalSelectionRects[0].top is initialSelectionRects[0].top
-PASS finalSelectionRects[0].left is initialSelectionRects[0].left
-PASS finalSelectionRects[0].width is initialSelectionRects[0].width
-PASS finalSelectionRects[0].height is initialSelectionRects[0].height
WebKit
<html>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<head>
+ <script src="../../../resources/ui-helper.js"></script>
<script src="../../../resources/js-test.js"></script>
<style>
body {
margin: 0;
}
- #fixed {
+ #absolute {
width: 100vw;
height: 50px;
position: absolute;
#content {
background-image: linear-gradient(blue, red);
- height: 4000px;
+ height: 10000px;
}
</style>
<script>
jsTestIsAsync = true;
- function toString(rect)
+ function selectTextAt(tapX, tapY)
{
- return `[${rect.left} ${rect.top} ; ${rect.width} ${rect.height}]`;
- }
-
- function selectFixedTextScript(tapX, tapY)
- {
- return `
- (function() {
- uiController.longPressAtPoint(${tapX}, ${tapY}, function() {
- uiController.doAfterNextStablePresentationUpdate(function() {
- uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
- });
- });
- })();`;
- }
-
- function simulateScrollingScript(distance)
- {
- return `(function() {
- uiController.stableStateOverride = false;
- uiController.immediateScrollToOffset(0, ${distance});
- uiController.stableStateOverride = true;
- uiController.doAfterNextStablePresentationUpdate(function() {
- uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
- });
- })()`;
- }
-
- function toString(rect)
- {
- return `[${rect.left} ${rect.top} ; ${rect.width} ${rect.height}]`;
+ return new Promise(resolve => {
+ testRunner.runUIScript(`
+ (function() {
+ uiController.longPressAtPoint(${tapX}, ${tapY}, function() {
+ uiController.uiScriptComplete("Done");
+ });
+ })();`, resolve);
+ });
}
function run()
{
if (!window.testRunner || !testRunner.runUIScript) {
- description("To manually test, long press this text and scroll up. The selection rect should be in the expected place (at the very top of the page) after scrolling.");
+ description("To manually test, long press this text and scroll up. The selection rect should be in the expected place (in the absolutely positioned div) after scrolling.");
return;
}
- testRunner.runUIScript(selectFixedTextScript(50, 50), initialSelectionRects => {
- initialSelectionRects = JSON.parse(initialSelectionRects);
- window.initialSelectionRects = initialSelectionRects;
- debug(`After long pressing, the selection rects are: ${initialSelectionRects.map(toString)}`);
- testRunner.runUIScript(simulateScrollingScript(1000), finalSelectionRects => {
- finalSelectionRects = JSON.parse(finalSelectionRects);
- window.finalSelectionRects = finalSelectionRects;
- debug(`After scrolling 1000px down, the selection rects are: ${finalSelectionRects.map(toString)}`);
- shouldBe("finalSelectionRects[0].top", "initialSelectionRects[0].top");
- shouldBe("finalSelectionRects[0].left", "initialSelectionRects[0].left");
- shouldBe("finalSelectionRects[0].width", "initialSelectionRects[0].width");
- shouldBe("finalSelectionRects[0].height", "initialSelectionRects[0].height");
- finishJSTest();
- });
+ selectTextAt(50, 50)
+ .then(() => UIHelper.getUISelectionRects())
+ .then((rects) => {
+ initialSelectionRects = rects;
+ shouldBe("initialSelectionRects.length", "1");
+ shouldBe("initialSelectionRects[0].left", "0");
+ shouldBe("initialSelectionRects[0].top", "0");
+ shouldBe("initialSelectionRects[0].width", "309");
+ shouldBe("initialSelectionRects[0].height", "114");
+ document.body.scrollTop += 5000;
+ return UIHelper.getUISelectionRects();
+ })
+ .then((rects) => {
+ finalSelectionRects = rects;
+ shouldBe("finalSelectionRects.length", "1");
+ shouldBe("finalSelectionRects[0].left", "0");
+ shouldBe("finalSelectionRects[0].top", "0");
+ shouldBe("finalSelectionRects[0].width", "309");
+ shouldBe("finalSelectionRects[0].height", "114");
+ finishJSTest();
});
}
</script>
</head>
<body onload=run()>
- <div id="fixed">WebKit</div>
+ <div id="absolute">WebKit</div>
<div id="content"></div>
</body>
-</html>
\ No newline at end of file
+</html>
+PASS initialSelectionRects.length is 1
+PASS initialSelectionRects[0].left is 0
+PASS initialSelectionRects[0].top is 0
+PASS initialSelectionRects[0].width is 309
+PASS initialSelectionRects[0].height is 114
+PASS finalSelectionRects.length is 1
+PASS finalSelectionRects[0].left is 0
+PASS finalSelectionRects[0].top is 5000
+PASS finalSelectionRects[0].width is 309
+PASS finalSelectionRects[0].height is 114
PASS successfullyParsed is true
TEST COMPLETE
-After long pressing, the selection rects are: [0 0 ; 309 114]
-After scrolling 1000px down, the selection rects are: [0 1000 ; 309 114]
-PASS initialSelectionRects.length is 1
-PASS finalSelectionRects.length is 1
-PASS finalSelectionRects[0].top - initialSelectionRects[0].top is 1000
WebKit
<html>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<head>
+ <script src="../../../resources/ui-helper.js"></script>
<script src="../../../resources/js-test.js"></script>
<style>
body {
#content {
background-image: linear-gradient(blue, red);
- height: 4000px;
+ height: 10000px;
}
</style>
<script>
jsTestIsAsync = true;
- function toString(rect)
+ function selectTextAt(tapX, tapY)
{
- return `[${rect.left} ${rect.top} ; ${rect.width} ${rect.height}]`;
- }
-
- function selectFixedTextScript(tapX, tapY)
- {
- return `
- (function() {
- uiController.longPressAtPoint(${tapX}, ${tapY}, function() {
- uiController.doAfterNextStablePresentationUpdate(function() {
- uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
- });
- });
- })();`;
- }
-
- function simulateScrollingScript(distance)
- {
- return `(function() {
- uiController.stableStateOverride = false;
- uiController.immediateScrollToOffset(0, ${distance});
- uiController.stableStateOverride = true;
- uiController.doAfterNextStablePresentationUpdate(function() {
- uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
- });
- })()`;
- }
-
- function toString(rect)
- {
- return `[${rect.left} ${rect.top} ; ${rect.width} ${rect.height}]`;
+ return new Promise(resolve => {
+ testRunner.runUIScript(`
+ (function() {
+ uiController.longPressAtPoint(${tapX}, ${tapY}, function() {
+ uiController.uiScriptComplete("Done");
+ });
+ })();`, resolve);
+ });
}
function run()
return;
}
- testRunner.runUIScript(selectFixedTextScript(50, 50), initialSelectionRects => {
- initialSelectionRects = JSON.parse(initialSelectionRects);
- window.initialSelectionRects = initialSelectionRects;
- debug(`After long pressing, the selection rects are: ${initialSelectionRects.map(toString)}`);
- testRunner.runUIScript(simulateScrollingScript(1000), finalSelectionRects => {
- finalSelectionRects = JSON.parse(finalSelectionRects);
- window.finalSelectionRects = finalSelectionRects;
- debug(`After scrolling 1000px down, the selection rects are: ${finalSelectionRects.map(toString)}`);
- shouldBe("initialSelectionRects.length", "1");
- shouldBe("finalSelectionRects.length", "1");
- shouldBe("finalSelectionRects[0].top - initialSelectionRects[0].top", "1000");
- finishJSTest();
- });
+ selectTextAt(50, 50)
+ .then(() => UIHelper.getUISelectionRects())
+ .then((rects) => {
+ initialSelectionRects = rects;
+ shouldBe("initialSelectionRects.length", "1");
+ shouldBe("initialSelectionRects[0].left", "0");
+ shouldBe("initialSelectionRects[0].top", "0");
+ shouldBe("initialSelectionRects[0].width", "309");
+ shouldBe("initialSelectionRects[0].height", "114");
+ document.body.scrollTop += 5000;
+ return UIHelper.getUISelectionRects();
+ })
+ .then((rects) => {
+ finalSelectionRects = rects;
+ shouldBe("finalSelectionRects.length", "1");
+ shouldBe("finalSelectionRects[0].left", "0");
+ shouldBe("finalSelectionRects[0].top", "5000");
+ shouldBe("finalSelectionRects[0].width", "309");
+ shouldBe("finalSelectionRects[0].height", "114");
+ finishJSTest();
});
}
</script>
<div id="fixed">WebKit</div>
<div id="content"></div>
</body>
-</html>
\ No newline at end of file
+</html>
RenderBlock {P} at (0,238) size 784x58 [border: (2px solid #0000FF)]
RenderText {#text} at (14,15) size 36x28
text run at (14,15) width 36: "xxx"
- RenderBlock (anonymous) at (0,320) size 784x0
RenderBlock {P} at (0,320) size 784x58 [border: (2px solid #0000FF)]
RenderBR {BR} at (14,15) size 0x28 [bgcolor=#008000]
RenderBlock {P} at (0,402) size 784x58 [border: (2px solid #0000FF)]
text run at (2,3) width 20: "fo"
RenderText {#text} at (21,3) size 13x28
text run at (21,3) width 13: "x"
- RenderBlock (anonymous) at (0,34) size 784x0
caret: position 3 of child 0 {#text} of child 0 {B} of child 1 {DIV} of child 3 {DIV} of body
RenderInline {FONT} at (0,0) size 159x19 [color=#0000FF]
RenderText {#text} at (150,0) size 159x19
text run at (150,0) width 159: "This text should be blue."
- RenderBlock (anonymous) at (0,76) size 784x0
caret: position 25 of child 0 {#text} of child 1 {FONT} of child 0 {SPAN} of child 2 {DIV} of body
webkit.org/b/168933 fast/regions/inline-block-inside-anonymous-overflow-with-covered-controls.html [ Pass ImageOnlyFailure ]
-webkit.org/b/168346 [ Sierra Debug ] editing/selection/move-by-word-visually-multi-space.html [ Pass Timeout ]
-
webkit.org/b/169621 imported/w3c/web-platform-tests/IndexedDB/fire-error-event-exception.html [ Pass Failure ]
webkit.org/b/169760 imported/w3c/web-platform-tests/IndexedDB/fire-success-event-exception.html [ Pass Failure ]
RenderText {#text} at (164,14) size 78x28
text run at (164,14) width 78: " xxxxxx"
RenderInline {SPAN} at (0,0) size 0x28
+ RenderBlock (anonymous) at (0,56) size 784x0
selection start: position 0 of child 1 {#text} of child 1 {DIV} of body
selection end: position 6 of child 1 {#text} of child 1 {DIV} of body
});
}
+ static ensurePresentationUpdate()
+ {
+ if (!this.isWebKit2()) {
+ testRunner.display();
+ return Promise.resolve();
+ }
+
+ return new Promise(resolve => {
+ testRunner.runUIScript(`
+ uiController.doAfterPresentationUpdate(function() {
+ uiController.uiScriptComplete('Done');
+ });`, resolve);
+ });
+ }
+
+ static activateAndWaitForInputSessionAt(x, y)
+ {
+ if (!this.isWebKit2() || !this.isIOS())
+ return this.activateAt(x, y);
+
+ return new Promise(resolve => {
+ testRunner.runUIScript(`
+ (function() {
+ uiController.didShowKeyboardCallback = function() {
+ uiController.uiScriptComplete("Done");
+ };
+ uiController.singleTapAtPoint(${x}, ${y}, function() { });
+ })()`, resolve);
+ });
+ }
+
+ static getUICaretRect()
+ {
+ if (!this.isWebKit2() || !this.isIOS())
+ return Promise.resolve();
+
+ return new Promise(resolve => {
+ testRunner.runUIScript(`(function() {
+ uiController.doAfterNextStablePresentationUpdate(function() {
+ uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionCaretRect));
+ });
+ })()`, jsonString => {
+ resolve(JSON.parse(jsonString));
+ });
+ });
+ }
+
+ static getUISelectionRects()
+ {
+ if (!this.isWebKit2() || !this.isIOS())
+ return Promise.resolve();
+
+ return new Promise(resolve => {
+ testRunner.runUIScript(`(function() {
+ uiController.doAfterNextStablePresentationUpdate(function() {
+ uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
+ });
+ })()`, jsonString => {
+ resolve(JSON.parse(jsonString));
+ });
+ });
+ }
+
static wait(promise)
{
testRunner.waitUntilDone();
+2017-08-22 Wenson Hsieh <wenson_hsieh@apple.com>
+
+ [WK2] EditorState updates should be rolled into the layer update lifecycle when possible
+ https://bugs.webkit.org/show_bug.cgi?id=175370
+ <rdar://problem/33799806>
+
+ Reviewed by Ryosuke Niwa.
+
+ Remove didChangeSelectionAndUpdateLayout -- EditorState updates that are scheduled due to missing post-layout
+ data will now be scheduled for the next presentation update. Additionally, add editor client hooks to notify the
+ WebKit layer when we've updated the current composition. See WebKit ChangeLog for more details. This patch
+ adjusts and rebaselines existing layout tests.
+
+ * editing/Editor.cpp:
+ (WebCore::SetCompositionScope::SetCompositionScope):
+ (WebCore::SetCompositionScope::~SetCompositionScope):
+
+ Introduce a helper RAII class to ensure that we ignore selection changes during the scope of
+ Editor::setComposition and call out to the client with WebEditorClient::didUpdateComposition afterwards. This
+ also maintains a UserTypingGestureIndicator over its lifetime, so we don't additionally need to create a
+ UserTypingGestureIndicator in Editor::setComposition.
+
+ (WebCore::Editor::setComposition):
+ * editing/FrameSelection.cpp:
+ (WebCore::FrameSelection::setSelection):
+ (WebCore::FrameSelection::updateAndRevealSelection):
+ (WebCore::FrameSelection::setSelectedRange):
+ * editing/FrameSelection.h:
+ (WebCore::FrameSelection::defaultSetSelectionOptions):
+
+ Plumb state about whether or not the selection change was triggered by the user to FrameSelection::setSelection,
+ and if so, notify the editing client. A separate SetSelectionOptions flag is used here instead of
+ RevealSelection to avoid calling out to the client in places where we want to reveal the selection regardless of
+ whether or not the selection is user triggered.
+
+ * loader/EmptyClients.cpp:
+ * page/EditorClient.h:
+
2017-08-21 Ryosuke Niwa <rniwa@webkit.org>
Consolidate the code to normalize MIME type in DataTransfer
setComposition(text, ConfirmComposition);
}
+class SetCompositionScope {
+public:
+ SetCompositionScope(Frame& frame)
+ : m_frame(frame)
+ , m_typingGestureIndicator(frame)
+ {
+ m_frame->editor().setIgnoreSelectionChanges(true);
+ }
+
+ ~SetCompositionScope()
+ {
+ m_frame->editor().setIgnoreSelectionChanges(false);
+ if (auto* editorClient = m_frame->editor().client())
+ editorClient->didUpdateComposition();
+ }
+
+ Ref<Frame> m_frame;
+ UserTypingGestureIndicator m_typingGestureIndicator;
+};
+
void Editor::setComposition(const String& text, SetCompositionMode mode)
{
ASSERT(mode == ConfirmComposition || mode == CancelComposition);
- UserTypingGestureIndicator typingGestureIndicator(m_frame);
-
- setIgnoreSelectionChanges(true);
+ SetCompositionScope setCompositionScope(m_frame);
if (mode == CancelComposition)
ASSERT(text == emptyString());
m_compositionNode = nullptr;
m_customCompositionUnderlines.clear();
- if (m_frame.selection().isNone()) {
- setIgnoreSelectionChanges(false);
+ if (m_frame.selection().isNone())
return;
- }
// Always delete the current composition before inserting the finalized composition text if we're confirming our composition.
// Our default behavior (if the beforeinput event is not prevented) is to insert the finalized composition text back in.
// An open typing command that disagrees about current selection would cause issues with typing later on.
TypingCommand::closeTyping(&m_frame);
}
-
- setIgnoreSelectionChanges(false);
}
void Editor::setComposition(const String& text, const Vector<CompositionUnderline>& underlines, unsigned selectionStart, unsigned selectionEnd)
{
- Ref<Frame> protection(m_frame);
-
- UserTypingGestureIndicator typingGestureIndicator(m_frame);
-
- setIgnoreSelectionChanges(true);
+ SetCompositionScope setCompositionScope(m_frame);
// Updates styles before setting selection for composition to prevent
// inserting the previous composition text into text nodes oddly.
selectComposition();
- if (m_frame.selection().isNone()) {
- setIgnoreSelectionChanges(false);
+ if (m_frame.selection().isNone())
return;
- }
String originalText = selectedText();
bool isStartingToRecomposeExistingRange = !text.isEmpty() && selectionStart < selectionEnd && !hasComposition();
}
}
- setIgnoreSelectionChanges(false);
-
#if PLATFORM(IOS)
client()->stopDelayingAndCoalescingContentChangeNotifications();
#endif
return;
updateAndRevealSelection(intent);
+
+ if (options & IsUserTriggered) {
+ if (auto* client = m_frame->editor().client())
+ client->didEndUserTriggeredSelectionChanges();
+ }
}
static void updateSelectionByUpdatingLayoutOrStyle(Frame& frame)
}
notifyAccessibilityForSelectionChange(intent);
-
- if (auto* client = m_frame->editor().client())
- client->didChangeSelectionAndUpdateLayout();
}
void FrameSelection::updateDataDetectorsForSelection()
return false;
}
- setSelection(newSelection, ClearTypingStyle | (closeTyping ? CloseTyping : 0));
+ setSelection(newSelection, ClearTypingStyle | (closeTyping ? CloseTyping : 0) | (userTriggered == UserTriggered ? IsUserTriggered : 0));
return true;
}
DoNotSetFocus = 1 << 4,
DictationTriggered = 1 << 5,
RevealSelection = 1 << 6,
+ IsUserTriggered = 1 << 7,
};
typedef unsigned SetSelectionOptions; // Union of values in SetSelectionOption and EUserTriggered
static inline SetSelectionOptions defaultSetSelectionOptions(EUserTriggered userTriggered = NotUserTriggered)
{
- return CloseTyping | ClearTypingStyle | (userTriggered ? (RevealSelection | FireSelectEvent) : 0);
+ return CloseTyping | ClearTypingStyle | (userTriggered ? (RevealSelection | FireSelectEvent | IsUserTriggered) : 0);
}
WEBCORE_EXPORT explicit FrameSelection(Frame* = nullptr);
void didBeginEditing() final { }
void respondToChangedContents() final { }
void respondToChangedSelection(Frame*) final { }
- void didChangeSelectionAndUpdateLayout() final { }
void updateEditorStateAfterLayoutIfEditabilityChanged() final { }
void discardedComposition(Frame*) final { }
void canceledComposition() final { }
+ void didUpdateComposition() final { }
void didEndEditing() final { }
+ void didEndUserTriggeredSelectionChanges() final { }
void willWriteSelectionToPasteboard(Range*) final { }
void didWriteSelectionToPasteboard() final { }
void getClientPasteboardDataForRange(Range*, Vector<String>&, Vector<RefPtr<SharedBuffer>>&) final { }
virtual void didBeginEditing() = 0;
virtual void respondToChangedContents() = 0;
virtual void respondToChangedSelection(Frame*) = 0;
- virtual void didChangeSelectionAndUpdateLayout() = 0;
+ virtual void didEndUserTriggeredSelectionChanges() = 0;
virtual void updateEditorStateAfterLayoutIfEditabilityChanged() = 0;
virtual void didEndEditing() = 0;
virtual void willWriteSelectionToPasteboard(Range*) = 0;
// This function is not called when a composition is closed per a request from an input method.
virtual void discardedComposition(Frame*) = 0;
virtual void canceledComposition() = 0;
+ virtual void didUpdateComposition() = 0;
virtual void registerUndoStep(UndoStep&) = 0;
virtual void registerRedoStep(UndoStep&) = 0;
+2017-08-22 Wenson Hsieh <wenson_hsieh@apple.com>
+
+ [WK2] EditorState updates should be rolled into the layer update lifecycle when possible
+ https://bugs.webkit.org/show_bug.cgi?id=175370
+ <rdar://problem/33799806>
+
+ Reviewed by Ryosuke Niwa.
+
+ See per-method comments for more detail. WebPage::didChangeSelection now schedules EditorState updates to be sent
+ during the next layer tree transaction rather than sending them synchronously. To ensure that iOS and Mac continue
+ to behave correctly w.r.t. EditorState updates, we immediately dispatch EditorStates in the following cases:
+ - After the composition changes, is confirmed, or is canceled.
+ - After an edit command is executed.
+ - After ending user-triggered selection changes.
+
+ * Shared/mac/RemoteLayerTreeTransaction.h:
+ (WebKit::RemoteLayerTreeTransaction::hasEditorState const):
+ (WebKit::RemoteLayerTreeTransaction::editorState const):
+ (WebKit::RemoteLayerTreeTransaction::setEditorState):
+
+ Attaches an optional EditorState to the RemoteLayerTreeTransaction. This EditorState is computed and sent over
+ when setting up the transaction in WebPage, if something previously scheduled an EditorState update.
+
+ * Shared/mac/RemoteLayerTreeTransaction.mm:
+ (WebKit::RemoteLayerTreeTransaction::encode const):
+ (WebKit::RemoteLayerTreeTransaction::decode):
+
+ Add coder support for sending over a layer tree transaction's EditorState.
+
+ * UIProcess/API/Cocoa/WKViewPrivate.h:
+ * UIProcess/API/mac/WKView.mm:
+ (-[WKView _doAfterNextPresentationUpdate:]):
+
+ Add _doAfterNextPresentationUpdate to WKView (used in TestWebKitAPI -- refer to
+ WebKitAgnosticTest::waitForNextPresentationUpdate).
+
+ * UIProcess/DrawingAreaProxy.h:
+ (WebKit::DrawingAreaProxy::dispatchPresentationCallbacksAfterFlushingLayers):
+ * UIProcess/DrawingAreaProxy.messages.in:
+
+ Add a new IPC messages, DispatchPresentationCallbacksAfterFlushingLayers, to invoke in-flight presentation
+ callbacks in the UI process following a layer flush in the web process.
+
+ * UIProcess/WebPageProxy.h:
+ * UIProcess/mac/RemoteLayerTreeDrawingAreaProxy.mm:
+ (WebKit::RemoteLayerTreeDrawingAreaProxy::commitLayerTree):
+ * UIProcess/mac/TiledCoreAnimationDrawingAreaProxy.h:
+ * UIProcess/mac/TiledCoreAnimationDrawingAreaProxy.mm:
+ (WebKit::TiledCoreAnimationDrawingAreaProxy::~TiledCoreAnimationDrawingAreaProxy):
+ (WebKit::TiledCoreAnimationDrawingAreaProxy::dispatchAfterEnsuringDrawing):
+ (WebKit::TiledCoreAnimationDrawingAreaProxy::dispatchPresentationCallbacksAfterFlushingLayers):
+
+ Run all pending _doAfterNextPresentationUpdate callbacks.
+
+ * WebProcess/WebCoreSupport/WebEditorClient.cpp:
+ (WebKit::WebEditorClient::didApplyStyle):
+ (WebKit::WebEditorClient::respondToChangedContents):
+ (WebKit::WebEditorClient::didEndUserTriggeredSelectionChanges):
+ (WebKit::WebEditorClient::didUpdateComposition):
+
+ Forward editor client calls to the WebPage.
+
+ (WebKit::WebEditorClient::didChangeSelectionAndUpdateLayout): Deleted.
+ * WebProcess/WebCoreSupport/WebEditorClient.h:
+ * WebProcess/WebPage/WebPage.cpp:
+ (WebKit::WebPage::editorState const):
+ (WebKit::WebPage::updateEditorStateAfterLayoutIfEditabilityChanged):
+ (WebKit::WebPage::willCommitLayerTree):
+ (WebKit::WebPage::didApplyStyle):
+
+ Allow style application to immediately trigger EditorState updates, if we're not currently ignoring selection
+ changes in the Editor.
+
+ (WebKit::WebPage::didChangeContents):
+
+ Allow applying top-level edit commands to immediately trigger EditorState updates, if we're not currently
+ ignoring selection changes in the Editor.
+
+ (WebKit::WebPage::didChangeSelection):
+ (WebKit::WebPage::didUpdateComposition):
+ (WebKit::WebPage::didEndUserTriggeredSelectionChanges):
+ (WebKit::WebPage::discardedComposition):
+ (WebKit::WebPage::canceledComposition):
+
+ When handling composition updates, always send an EditorState to the UI process. Unlike other cases, IME
+ requires immediate EditorState data, so we need to be explicit here in sending updates right away.
+
+ (WebKit::WebPage::sendEditorStateUpdate):
+ (WebKit::WebPage::sendPartialEditorStateAndSchedulePostLayoutUpdate):
+ (WebKit::WebPage::flushPendingEditorStateUpdate):
+
+ Helper methods to schedule an EditorState update to be sent upon the next layer tree update, or flush any
+ pending EditorState update that has been scheduled. The private, more aggressive variant of this is
+ sendEditorStateUpdate, which ignores whether or not there was already an EditorState update scheduled, and sends
+ one anyways (this still fulfills any EditorState update that was previously scheduled).
+
+ These helper methods are treated as no-ops when invoked while ignoring selection changes. This is to prevent
+ temporary selection state and editor commands during operations such as text indicator snapshotting from pushing
+ bogus information about transient editor states to the UI process.
+
+ (WebKit::WebPage::sendPostLayoutEditorStateIfNeeded): Deleted.
+ * WebProcess/WebPage/WebPage.h:
+ * WebProcess/WebPage/ios/WebPageIOS.mm:
+ (WebKit::WebPage::platformEditorState const):
+ (WebKit::WebPage::executeEditCommandWithCallback):
+ (WebKit::selectionIsInsideFixedPositionContainer):
+ (WebKit::WebPage::updateVisibleContentRects):
+
+ Fix a hack that was computing an EditorState to figure out whether the current selection starts or ends in a
+ fixed position container. Factors out relevant logic into a separate helper, and also schedules an EditorState
+ update instead of immediately computing it.
+
+ * WebProcess/WebPage/mac/TiledCoreAnimationDrawingArea.h:
+ * WebProcess/WebPage/mac/TiledCoreAnimationDrawingArea.mm:
+ (WebKit::TiledCoreAnimationDrawingArea::addTransactionCallbackID):
+
+ Add support for registering and dispatching presentation callbacks that hook into the layer flush lifecycle,
+ using the tiled CA drawing area. These are used by Mac LayoutTests and API tests that need to wait until the
+ next flush before checking for state that depends on EditorState updates in the UI process.
+
+ (WebKit::TiledCoreAnimationDrawingArea::flushLayers):
+
+ Tell the WebPage to flush any pending EditorState updates.
+
+ * WebProcess/WebPage/mac/WebPageMac.mm:
+ (WebKit::WebPage::platformEditorState const):
+
2017-08-22 Brent Fulgham <bfulgham@apple.com>
Relax keychain access to permit users to permanently allow client certificates
#ifndef RemoteLayerTreeTransaction_h
#define RemoteLayerTreeTransaction_h
+#include "EditorState.h"
#include "GenericCallback.h"
#include "PlatformCAAnimationRemote.h"
#include "RemoteLayerBackingStore.h"
WebCore::LayoutMilestones newlyReachedLayoutMilestones() const { return m_newlyReachedLayoutMilestones; }
void setNewlyReachedLayoutMilestones(WebCore::LayoutMilestones milestones) { m_newlyReachedLayoutMilestones = milestones; }
+
+ bool hasEditorState() const { return !!m_editorState; }
+ const EditorState& editorState() const { return m_editorState.value(); }
+ void setEditorState(const EditorState& editorState) { m_editorState = editorState; }
private:
WebCore::GraphicsLayer::PlatformLayerID m_rootLayerID;
bool m_viewportMetaTagWidthWasExplicit { false };
bool m_viewportMetaTagCameFromImageDocument { false };
bool m_isInStableState { false };
+
+ std::optional<EditorState> m_editorState { std::nullopt };
};
} // namespace WebKit
encoder << m_isInStableState;
encoder << m_callbackIDs;
+
+ encoder << hasEditorState();
+ if (m_editorState)
+ encoder << *m_editorState;
}
bool RemoteLayerTreeTransaction::decode(IPC::Decoder& decoder, RemoteLayerTreeTransaction& result)
if (!decoder.decode(result.m_callbackIDs))
return false;
+ bool hasEditorState;
+ if (!decoder.decode(hasEditorState))
+ return false;
+
+ if (hasEditorState) {
+ EditorState editorState;
+ if (!decoder.decode(editorState))
+ return false;
+ result.setEditorState(editorState);
+ }
+
return true;
}
- (void)_addMediaPlaybackControlsView:(id)mediaPlaybackControlsView;
- (void)_removeMediaPlaybackControlsView;
+- (void)_doAfterNextPresentationUpdate:(void (^)(void))updateBlock WK_API_AVAILABLE(macosx(WK_MAC_TBA));
+
@end
#endif // !TARGET_OS_IPHONE
{
}
+- (void)_doAfterNextPresentationUpdate:(void (^)(void))updateBlock
+{
+ auto updateBlockCopy = makeBlockPtr(updateBlock);
+ _data->_impl->page().callAfterNextPresentationUpdate([updateBlockCopy](WebKit::CallbackBase::Error error) {
+ if (updateBlockCopy)
+ updateBlockCopy();
+ });
+}
+
@end
#endif // PLATFORM(MAC)
virtual WebCore::MachSendRight createFence();
#endif
+ virtual void dispatchPresentationCallbacksAfterFlushingLayers(const Vector<CallbackID>&) { }
+
protected:
explicit DrawingAreaProxy(DrawingAreaType, WebPageProxy&);
EnterAcceleratedCompositingMode(uint64_t backingStoreStateID, WebKit::LayerTreeContext context)
ExitAcceleratedCompositingMode(uint64_t backingStoreStateID, WebKit::UpdateInfo updateInfo)
UpdateAcceleratedCompositingMode(uint64_t backingStoreStateID, WebKit::LayerTreeContext context)
+ DispatchPresentationCallbacksAfterFlushingLayers(Vector<WebKit::CallbackID> callbackIDs);
#if PLATFORM(COCOA)
// Used by TiledCoreAnimationDrawingAreaProxy.
#if PLATFORM(COCOA)
void createSandboxExtensionsIfNeeded(const Vector<String>& files, SandboxExtension::Handle& fileReadHandle, SandboxExtension::HandleArray& fileUploadHandles);
#endif
+ void editorStateChanged(const EditorState&);
private:
WebPageProxy(PageClient&, WebProcessProxy&, uint64_t pageID, Ref<API::PageConfiguration>&&);
void didEndColorPicker() override;
#endif
- void editorStateChanged(const EditorState&);
void compositionWasCanceled();
void setHasHadSelectionChangesFromUserInteraction(bool);
void setNeedsHiddenContentEditableQuirk(bool);
ASSERT(layerTreeTransaction.transactionID() == m_lastVisibleTransactionID + 1);
m_transactionIDForPendingCACommit = layerTreeTransaction.transactionID();
+ if (layerTreeTransaction.hasEditorState())
+ m_webPageProxy.editorStateChanged(layerTreeTransaction.editorState());
+
if (m_remoteLayerTreeHost.updateLayerTree(layerTreeTransaction)) {
if (layerTreeTransaction.transactionID() >= m_transactionIDForUnhidingContent)
m_webPageProxy.setAcceleratedCompositingRootLayer(m_remoteLayerTreeHost.rootLayer());
void waitForDidUpdateActivityState() override;
void dispatchAfterEnsuringDrawing(WTF::Function<void (CallbackBase::Error)>&&) override;
+ void dispatchPresentationCallbacksAfterFlushingLayers(const Vector<CallbackID>&) final;
void willSendUpdateGeometry() override;
// The last minimum layout size we sent to the web process.
WebCore::IntSize m_lastSentMinimumLayoutSize;
+
+ CallbackMap m_callbacks;
};
} // namespace WebKit
TiledCoreAnimationDrawingAreaProxy::~TiledCoreAnimationDrawingAreaProxy()
{
+ m_callbacks.invalidate(CallbackBase::Error::OwnerWasInvalidated);
}
void TiledCoreAnimationDrawingAreaProxy::deviceScaleFactorDidChange()
void TiledCoreAnimationDrawingAreaProxy::dispatchAfterEnsuringDrawing(WTF::Function<void (CallbackBase::Error)>&& callback)
{
- // This callback is primarily used for testing in RemoteLayerTreeDrawingArea. We could in theory wait for a CA commit here.
- dispatch_async(dispatch_get_main_queue(), BlockPtr<void ()>::fromCallable([callback = WTFMove(callback)] {
- callback(CallbackBase::Error::None);
- }).get());
+ if (!m_webPageProxy.isValid()) {
+ callback(CallbackBase::Error::OwnerWasInvalidated);
+ return;
+ }
+
+ m_webPageProxy.process().send(Messages::DrawingArea::AddTransactionCallbackID(m_callbacks.put(WTFMove(callback), nullptr)), m_webPageProxy.pageID());
+}
+
+void TiledCoreAnimationDrawingAreaProxy::dispatchPresentationCallbacksAfterFlushingLayers(const Vector<CallbackID>& callbackIDs)
+{
+ for (auto& callbackID : callbackIDs) {
+ if (auto callback = m_callbacks.take<VoidCallback>(callbackID))
+ callback->performCallback();
+ }
}
} // namespace WebKit
void WebEditorClient::didApplyStyle()
{
- notImplemented();
+ m_page->didApplyStyle();
}
bool WebEditorClient::shouldMoveRangeAfterDelete(Range*, Range*)
{
static NeverDestroyed<String> WebViewDidChangeNotification(MAKE_STATIC_STRING_IMPL("WebViewDidChangeNotification"));
m_page->injectedBundleEditorClient().didChange(*m_page, WebViewDidChangeNotification.get().impl());
- notImplemented();
+ m_page->didChangeContents();
}
void WebEditorClient::respondToChangedSelection(Frame* frame)
#endif
}
-void WebEditorClient::didChangeSelectionAndUpdateLayout()
+void WebEditorClient::didEndUserTriggeredSelectionChanges()
{
- m_page->sendPostLayoutEditorStateIfNeeded();
+ m_page->didEndUserTriggeredSelectionChanges();
}
void WebEditorClient::updateEditorStateAfterLayoutIfEditabilityChanged()
m_page->updateEditorStateAfterLayoutIfEditabilityChanged();
}
+void WebEditorClient::didUpdateComposition()
+{
+ m_page->didUpdateComposition();
+}
+
void WebEditorClient::discardedComposition(Frame*)
{
m_page->discardedComposition();
void didBeginEditing() final;
void respondToChangedContents() final;
void respondToChangedSelection(WebCore::Frame*) final;
- void didChangeSelectionAndUpdateLayout() final;
+ void didEndUserTriggeredSelectionChanges() final;
void updateEditorStateAfterLayoutIfEditabilityChanged() final;
void discardedComposition(WebCore::Frame*) final;
void canceledComposition() final;
+ void didUpdateComposition() final;
void didEndEditing() final;
void willWriteSelectionToPasteboard(WebCore::Range*) final;
void didWriteSelectionToPasteboard() final;
result.shouldIgnoreSelectionChanges = frame.editor().ignoreSelectionChanges();
#if PLATFORM(COCOA)
- if (shouldIncludePostLayoutData == IncludePostLayoutDataHint::Yes && result.isContentEditable) {
+ bool canIncludePostLayoutData = frame.view() && !frame.view()->needsLayout();
+ if (shouldIncludePostLayoutData == IncludePostLayoutDataHint::Yes && canIncludePostLayoutData && result.isContentEditable) {
auto& postLayoutData = result.postLayoutData();
if (!selection.isNone()) {
Node* nodeToRemove;
Frame& frame = m_page->focusController().focusedOrMainFrame();
EditorStateIsContentEditable editorStateIsContentEditable = frame.selection().selection().isContentEditable() ? EditorStateIsContentEditable::Yes : EditorStateIsContentEditable::No;
if (m_lastEditorStateWasContentEditable != editorStateIsContentEditable)
- send(Messages::WebPageProxy::EditorStateChanged(editorState()));
+ sendPartialEditorStateAndSchedulePostLayoutUpdate();
}
String WebPage::renderTreeExternalRepresentation() const
#if PLATFORM(MAC)
layerTransaction.setScrollPosition(frameView->scrollPosition());
#endif
+
+ if (m_hasPendingEditorStateUpdate) {
+ layerTransaction.setEditorState(editorState());
+ m_hasPendingEditorStateUpdate = false;
+ }
}
void WebPage::didFlushLayerTreeAtTime(MonotonicTime timestamp)
}
#endif
+void WebPage::didApplyStyle()
+{
+ sendEditorStateUpdate();
+}
+
+void WebPage::didChangeContents()
+{
+ sendEditorStateUpdate();
+}
+
void WebPage::didChangeSelection()
{
Frame& frame = m_page->focusController().focusedOrMainFrame();
if (m_isSelectingTextWhileInsertingAsynchronously)
return;
- FrameView* view = frame.view();
-
- // 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(view && view->needsLayout() ? IncludePostLayoutDataHint::No : IncludePostLayoutDataHint::Yes);
- m_isEditorStateMissingPostLayoutData = editorState.isMissingPostLayoutData;
-
#if PLATFORM(MAC)
bool hasPreviouslyFocusedDueToUserInteraction = m_hasEverFocusedElementDueToUserInteractionSincePageTransition;
m_hasEverFocusedElementDueToUserInteractionSincePageTransition |= m_userIsInteracting;
if (frame.editor().hasComposition() && !frame.editor().ignoreSelectionChanges() && !frame.selection().isNone()) {
frame.editor().cancelComposition();
discardedComposition();
- } else
- send(Messages::WebPageProxy::EditorStateChanged(editorState));
-#else
- send(Messages::WebPageProxy::EditorStateChanged(editorState), pageID(), IPC::SendOption::DispatchMessageEvenWhenWaitingForSyncReply);
+ return;
+ }
#endif
-#if PLATFORM(IOS)
- m_drawingArea->scheduleCompositingLayerFlush();
-#endif
+ sendPartialEditorStateAndSchedulePostLayoutUpdate();
}
void WebPage::resetAssistedNodeForFrame(WebFrame* frame)
}
}
-void WebPage::sendPostLayoutEditorStateIfNeeded()
+void WebPage::didUpdateComposition()
{
- if (!m_isEditorStateMissingPostLayoutData)
- return;
+ sendEditorStateUpdate();
+}
- send(Messages::WebPageProxy::EditorStateChanged(editorState(IncludePostLayoutDataHint::Yes)), pageID(), IPC::SendOption::DispatchMessageEvenWhenWaitingForSyncReply);
- m_isEditorStateMissingPostLayoutData = false;
+void WebPage::didEndUserTriggeredSelectionChanges()
+{
+ Frame& frame = m_page->focusController().focusedOrMainFrame();
+ if (!frame.editor().ignoreSelectionChanges())
+ sendEditorStateUpdate();
}
void WebPage::discardedComposition()
{
send(Messages::WebPageProxy::CompositionWasCanceled());
- send(Messages::WebPageProxy::EditorStateChanged(editorState()));
+ sendEditorStateUpdate();
}
void WebPage::canceledComposition()
{
send(Messages::WebPageProxy::CompositionWasCanceled());
+ sendEditorStateUpdate();
}
void WebPage::setMinimumLayoutSize(const IntSize& minimumLayoutSize)
m_loaderClient->featuresUsedInPage(*this, namedFeatures);
}
+void WebPage::sendEditorStateUpdate()
+{
+ Frame& frame = m_page->focusController().focusedOrMainFrame();
+ if (frame.editor().ignoreSelectionChanges())
+ return;
+
+ m_hasPendingEditorStateUpdate = false;
+
+ // If we immediately dispatch an EditorState update to the UI process, layout may not be up to date yet.
+ // If that is the case, just send what we have (i.e. don't include post-layout data) and wait until the
+ // next layer tree commit to compute and send the complete EditorState over.
+ auto state = editorState();
+ send(Messages::WebPageProxy::EditorStateChanged(state), pageID(), IPC::SendOption::DispatchMessageEvenWhenWaitingForSyncReply);
+
+ if (state.isMissingPostLayoutData) {
+ m_hasPendingEditorStateUpdate = true;
+ m_drawingArea->scheduleCompositingLayerFlush();
+ }
+}
+
+void WebPage::sendPartialEditorStateAndSchedulePostLayoutUpdate()
+{
+ Frame& frame = m_page->focusController().focusedOrMainFrame();
+ if (frame.editor().ignoreSelectionChanges())
+ return;
+
+ send(Messages::WebPageProxy::EditorStateChanged(editorState(IncludePostLayoutDataHint::No)), pageID(), IPC::SendOption::DispatchMessageEvenWhenWaitingForSyncReply);
+
+ if (m_hasPendingEditorStateUpdate)
+ return;
+
+ // Flag the next layer flush to compute and propagate an EditorState to the UI process.
+ m_hasPendingEditorStateUpdate = true;
+ m_drawingArea->scheduleCompositingLayerFlush();
+}
+
+void WebPage::flushPendingEditorStateUpdate()
+{
+ if (!m_hasPendingEditorStateUpdate)
+ return;
+
+ Frame& frame = m_page->focusController().focusedOrMainFrame();
+ if (frame.editor().ignoreSelectionChanges())
+ return;
+
+ sendEditorStateUpdate();
+}
+
void WebPage::updateWebsitePolicies(const WebsitePolicies& websitePolicies)
{
if (!m_page)
enum class IncludePostLayoutDataHint { No, Yes };
EditorState editorState(IncludePostLayoutDataHint = IncludePostLayoutDataHint::Yes) const;
- void sendPostLayoutEditorStateIfNeeded();
void updateEditorStateAfterLayoutIfEditabilityChanged();
String renderTreeExternalRepresentation() const;
void didApplyStyle();
void didChangeSelection();
+ void didChangeContents();
void discardedComposition();
void canceledComposition();
+ void didUpdateComposition();
+ void didEndUserTriggeredSelectionChanges();
#if PLATFORM(COCOA)
void registerUIProcessAccessibilityTokens(const IPC::DataReference& elemenToken, const IPC::DataReference& windowToken);
static PluginView* pluginViewForFrame(WebCore::Frame*);
+ void sendPartialEditorStateAndSchedulePostLayoutUpdate();
+ void flushPendingEditorStateUpdate();
+
private:
WebPage(uint64_t pageID, WebPageCreationParameters&&);
void platformInitialize();
void platformDetach();
void platformEditorState(WebCore::Frame&, EditorState& result, IncludePostLayoutDataHint) const;
+ void sendEditorStateUpdate();
void didReceiveWebPageMessage(IPC::Connection&, IPC::Decoder&);
void didReceiveSyncWebPageMessage(IPC::Connection&, IPC::Decoder&, std::unique_ptr<IPC::Encoder>&);
RefPtr<WebCore::Node> m_assistedNode;
bool m_hasPendingBlurNotification { false };
+ bool m_hasPendingEditorStateUpdate { false };
#if PLATFORM(IOS)
RefPtr<WebCore::Range> m_currentWordRange;
// 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()) {
+ bool frameViewHasFinishedLayout = frame.view() && !frame.view()->needsLayout();
+ if (shouldIncludePostLayoutData == IncludePostLayoutDataHint::No && !frameViewHasFinishedLayout && !frame.editor().hasComposition()) {
result.isMissingPostLayoutData = true;
return;
}
void WebPage::executeEditCommandWithCallback(const String& commandName, CallbackID callbackID)
{
executeEditCommand(commandName, String());
- if (commandName == "toggleBold" || commandName == "toggleItalic" || commandName == "toggleUnderline")
- send(Messages::WebPageProxy::EditorStateChanged(editorState()));
send(Messages::WebPageProxy::VoidCallback(callbackID));
}
return scaleFromUIProcess;
}
+static bool selectionIsInsideFixedPositionContainer(Frame& frame)
+{
+ auto& selection = frame.selection().selection();
+ if (selection.isNone())
+ return false;
+
+ bool isInsideFixedPosition = false;
+ if (selection.isCaret()) {
+ frame.selection().absoluteCaretBounds(&isInsideFixedPosition);
+ return isInsideFixedPosition;
+ }
+
+ selection.visibleStart().absoluteCaretBounds(&isInsideFixedPosition);
+ if (isInsideFixedPosition)
+ return true;
+
+ selection.visibleEnd().absoluteCaretBounds(&isInsideFixedPosition);
+ return isInsideFixedPosition;
+}
+
void WebPage::updateVisibleContentRects(const VisibleContentRectUpdateInfo& visibleContentRectUpdateInfo, MonotonicTime oldestTimestamp)
{
LOG_WITH_STREAM(VisibleRects, stream << "\nWebPage::updateVisibleContentRects " << visibleContentRectUpdateInfo);
hasSetPageScale = true;
}
- FrameView& frameView = *m_page->mainFrame().view();
+ auto& frame = m_page->mainFrame();
+ FrameView& frameView = *frame.view();
if (scrollPosition != frameView.scrollPosition())
m_dynamicSizeUpdateHistory.clear();
if (m_isInStableState) {
if (frameView.frame().settings().visualViewportEnabled()) {
frameView.setLayoutViewportOverrideRect(LayoutRect(visibleContentRectUpdateInfo.customFixedPositionRect()));
- const auto& state = editorState();
- if (!state.isMissingPostLayoutData && state.postLayoutData().insideFixedPosition) {
+ if (selectionIsInsideFixedPositionContainer(frame)) {
+ // Ensure that the next layer tree commit contains up-to-date caret/selection rects.
frameView.frame().selection().setCaretRectNeedsUpdate();
- send(Messages::WebPageProxy::EditorStateChanged(state));
+ sendPartialEditorStateAndSchedulePostLayoutUpdate();
}
} else
frameView.setCustomFixedPositionLayoutRect(enclosingIntRect(visibleContentRectUpdateInfo.customFixedPositionRect()));
#if !PLATFORM(IOS)
+#include "CallbackID.h"
#include "DrawingArea.h"
#include "LayerTreeContext.h"
#include <WebCore/FloatRect.h>
void setColorSpace(const ColorSpaceData&) override;
void addFence(const WebCore::MachSendRight&) override;
+ void addTransactionCallbackID(CallbackID) override;
void setShouldScaleViewToFitDocument(bool) override;
void adjustTransientZoom(double scale, WebCore::FloatPoint origin) override;
WebCore::IntSize m_lastDocumentSizeForScaleToFit;
WebCore::LayoutMilestones m_pendingNewlyReachedLayoutMilestones { 0 };
+ Vector<CallbackID> m_pendingCallbackIDs;
};
} // namespace WebKit
m_pendingNewlyReachedLayoutMilestones = 0;
}
+void TiledCoreAnimationDrawingArea::addTransactionCallbackID(CallbackID callbackID)
+{
+ m_pendingCallbackIDs.append(callbackID);
+ scheduleCompositingLayerFlush();
+}
+
bool TiledCoreAnimationDrawingArea::flushLayers()
{
ASSERT(!m_layerTreeStateIsFrozen);
scaleViewToFitDocumentIfNeeded();
m_webPage.layoutIfNeeded();
+ m_webPage.flushPendingEditorStateUpdate();
updateIntrinsicContentSizeIfNeeded();
if (m_transientZoomScale != 1)
applyTransientZoomToLayers(m_transientZoomScale, m_transientZoomOrigin);
+ if (!m_pendingCallbackIDs.isEmpty()) {
+ m_webPage.send(Messages::DrawingAreaProxy::DispatchPresentationCallbacksAfterFlushingLayers(m_pendingCallbackIDs));
+ m_pendingCallbackIDs.clear();
+ }
+
return returnValue;
}
}
void WebPage::platformEditorState(Frame& frame, EditorState& result, IncludePostLayoutDataHint shouldIncludePostLayoutData) const
{
- if (shouldIncludePostLayoutData == IncludePostLayoutDataHint::No || !result.isContentEditable) {
+ if (shouldIncludePostLayoutData == IncludePostLayoutDataHint::No || !frame.view() || frame.view()->needsLayout() || !result.isContentEditable) {
result.isMissingPostLayoutData = true;
return;
}
+2017-08-22 Wenson Hsieh <wenson_hsieh@apple.com>
+
+ [WK2] EditorState updates should be rolled into the layer update lifecycle when possible
+ https://bugs.webkit.org/show_bug.cgi?id=175370
+ <rdar://problem/33799806>
+
+ Reviewed by Ryosuke Niwa.
+
+ Adjust WebEditorClient for interface changes.
+
+ * WebCoreSupport/WebEditorClient.h:
+
2017-08-22 Alex Christensen <achristensen@webkit.org>
Remove ChromeClient::scrollbarsModeDidChange
void respondToChangedContents() final;
void respondToChangedSelection(WebCore::Frame*) final;
- void didChangeSelectionAndUpdateLayout() final { }
+ void didEndUserTriggeredSelectionChanges() final { }
void updateEditorStateAfterLayoutIfEditabilityChanged() final;
void discardedComposition(WebCore::Frame*) final;
void canceledComposition() final;
+ void didUpdateComposition() final { }
void registerUndoStep(WebCore::UndoStep&) final;
void registerRedoStep(WebCore::UndoStep&) final;
+2017-08-22 Wenson Hsieh <wenson_hsieh@apple.com>
+
+ [WK2] EditorState updates should be rolled into the layer update lifecycle when possible
+ https://bugs.webkit.org/show_bug.cgi?id=175370
+ <rdar://problem/33799806>
+
+ Reviewed by Ryosuke Niwa.
+
+ Adjust WebEditorClient for interface changes.
+
+ * WebCoreSupport/WebEditorClient.h:
+
2017-08-22 Brent Fulgham <bfulgham@apple.com>
Unreviewed build fix after r221017.
void respondToChangedContents() final;
void respondToChangedSelection(WebCore::Frame*) final;
- void didChangeSelectionAndUpdateLayout() final { }
void updateEditorStateAfterLayoutIfEditabilityChanged() final { }
void canceledComposition() final;
void discardedComposition(WebCore::Frame*) final;
+ void didUpdateComposition() final { }
bool shouldDeleteRange(WebCore::Range*) final;
+2017-08-22 Wenson Hsieh <wenson_hsieh@apple.com>
+
+ [WK2] EditorState updates should be rolled into the layer update lifecycle when possible
+ https://bugs.webkit.org/show_bug.cgi?id=175370
+ <rdar://problem/33799806>
+
+ Reviewed by Ryosuke Niwa.
+
+ Tweaks API tests that involve editing to wait for a presentation update before checking against UI process-side
+ information sent via EditorState updates. This allows any EditorState update scheduled by the test to propagate
+ to the UI process.
+
+ * TestWebKitAPI/Tests/WebKit2Cocoa/WKWebViewCandidateTests.mm:
+ (-[CandidateTestWebView typeString:inputMessage:]):
+ (+[CandidateTestWebView setUpWithFrame:testPage:]):
+ * TestWebKitAPI/Tests/WebKit2Cocoa/WKWebViewTextInput.mm:
+ * TestWebKitAPI/Tests/mac/AcceptsFirstMouse.mm:
+ (TestWebKitAPI::AcceptsFirstMouse::runTest):
+ * TestWebKitAPI/Tests/mac/WKWebViewMacEditingTests.mm:
+ * TestWebKitAPI/cocoa/TestWKWebView.h:
+ * TestWebKitAPI/cocoa/TestWKWebView.mm:
+ (-[TestWKWebView waitForNextPresentationUpdate]):
+
+ Add a new helper method to spin until the next presentation update.
+
+ * TestWebKitAPI/mac/WebKitAgnosticTest.h:
+ * TestWebKitAPI/mac/WebKitAgnosticTest.mm:
+ (TestWebKitAPI::WebKitAgnosticTest::waitForNextPresentationUpdate):
+
2017-08-22 Alex Christensen <achristensen@webkit.org>
Add UIDelegatePrivate SPI corresponding to WKPageUIClient.showPage
[self typeCharacter:[string characterAtIndex:i]];
});
[self waitForMessage:inputMessage];
+ [self waitForNextPresentationUpdate];
}
}
[wkWebView loadTestPageNamed:testPageName];
[wkWebView waitForMessage:@"focused"];
+ [wkWebView waitForNextPresentationUpdate];
[wkWebView _forceRequestCandidates];
return wkWebView;
CandidateTestWebView *wkWebView = [CandidateTestWebView setUpWithFrame:NSMakeRect(0, 0, 800, 600) testPage:@"input-field-in-scrollable-document"];
[wkWebView insertCandidatesAndWaitForResponse:@"apple " range:NSMakeRange(0, 0)];
- EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"apple "]);
+ EXPECT_WK_STREQ("apple ", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
[wkWebView expectCandidateListVisibilityUpdates:0 whenPerformingActions:^()
{
[wkWebView typeString:@" " inputMessage:@"input"];
}];
- EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"apple "]);
+ EXPECT_WK_STREQ("apple ", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
EXPECT_EQ([[wkWebView stringByEvaluatingJavaScript:GetDocumentScrollTopJSExpression] doubleValue], 0);
}
CandidateTestWebView *wkWebView = [CandidateTestWebView setUpWithFrame:NSMakeRect(0, 0, 800, 600) testPage:@"input-field-in-scrollable-document"];
[wkWebView insertCandidatesAndWaitForResponse:@"foo " range:NSMakeRange(0, 0)];
- EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"foo "]);
+ EXPECT_WK_STREQ("foo ", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
[wkWebView typeString:@"a" inputMessage:@"input"];
- EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"foo a"]);
+ EXPECT_WK_STREQ("foo a", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
}
TEST(WKWebViewCandidateTests, InsertCandidateFromPartiallyTypedPhraseWithSoftSpace)
[wkWebView typeString:@"hel" inputMessage:@"input"];
[wkWebView insertCandidatesAndWaitForResponse:@"hello " range:NSMakeRange(0, 3)];
- EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"hello "]);
+ EXPECT_WK_STREQ("hello ", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
[wkWebView expectCandidateListVisibilityUpdates:0 whenPerformingActions:^()
{
[wkWebView typeString:@" " inputMessage:@"input"];
- EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"hello "]);
+ EXPECT_WK_STREQ("hello ", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
EXPECT_EQ([[wkWebView stringByEvaluatingJavaScript:GetDocumentScrollTopJSExpression] doubleValue], 0);
[wkWebView typeString:@" " inputMessage:@"input"];
- EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"hello "]);
+ EXPECT_WK_STREQ("hello ", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
EXPECT_EQ([[wkWebView stringByEvaluatingJavaScript:GetDocumentScrollTopJSExpression] doubleValue], 0);
}];
}
});
[wkWebView waitForMessage:@"mousedown"];
}];
- EXPECT_TRUE([[wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression] isEqualToString:@"test"]);
+ EXPECT_WK_STREQ("test", [wkWebView stringByEvaluatingJavaScript:GetInputValueJSExpression]);
}
TEST(WKWebViewCandidateTests, ShouldNotRequestCandidatesInPasswordField)
#import "PlatformUtilities.h"
#import "Test.h"
#import "TestNavigationDelegate.h"
+#import "TestWKWebView.h"
#import <WebKit/WebKit.h>
#import <wtf/RetainPtr.h>
TEST(WKWebView, ShouldHaveInputContextForEditableContent)
{
- RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]);
-
- NSURLRequest *request = [NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:@"editable-body" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]];
- [webView loadRequest:request];
- [webView _test_waitForDidFinishNavigation];
+ auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]);
+ [webView synchronouslyLoadTestPageNamed:@"editable-body"];
+ [webView waitForNextPresentationUpdate];
EXPECT_NOT_NULL([webView inputContext]);
}
{
RetainPtr<NSWindow> window = adoptNS([[NSWindow alloc] initWithContentRect:view.frame styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:YES]);
[[window.get() contentView] addSubview:view];
+ waitForNextPresentationUpdate(view);
CGFloat viewHeight = view.bounds.size.height;
SlowInputWebView *webView = [[[SlowInputWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400)] autorelease];
[webView synchronouslyLoadHTMLString:[NSString stringWithFormat:@"<body contenteditable>Hello world</body>"]];
[webView stringByEvaluatingJavaScript:@"document.body.focus()"];
+ [webView waitForNextPresentationUpdate];
[webView removeFromSuperview];
[webView typeCharacter:'a'];
- (NSString *)stringByEvaluatingJavaScript:(NSString *)script;
- (void)waitForMessage:(NSString *)message;
- (void)performAfterLoading:(dispatch_block_t)actions;
+- (void)waitForNextPresentationUpdate;
@end
#if PLATFORM(IOS)
[contentController addScriptMessageHandler:handler name:@"onloadHandler"];
}
+- (void)waitForNextPresentationUpdate
+{
+ __block bool done = false;
+ [self _doAfterNextPresentationUpdate:^() {
+ done = true;
+ }];
+
+ TestWebKitAPI::Util::run(&done);
+}
+
@end
#if PLATFORM(IOS)
void goBack(WebView *);
void goBack(WKView *);
+ void waitForNextPresentationUpdate(WebView *);
+ void waitForNextPresentationUpdate(WKView *);
+
void waitForLoadToFinish();
NSRect viewFrame;
didFinishLoad = false;
}
+void WebKitAgnosticTest::waitForNextPresentationUpdate(WebView *)
+{
+ // FIXME: This isn't currently required anywhere. Just dispatch to the next runloop for now.
+ __block bool done = false;
+ dispatch_async(dispatch_get_main_queue(), ^() {
+ done = true;
+ });
+ Util::run(&done);
+}
+
+void WebKitAgnosticTest::waitForNextPresentationUpdate(WKView *view)
+{
+ __block bool done = false;
+ [view _doAfterNextPresentationUpdate:^() {
+ done = true;
+ }];
+ Util::run(&done);
+}
+
} // namespace TestWebKitAPI