Skip to content

Commit

Permalink
[macOS] If you scroll the page without moving the mouse, we fail to u…
Browse files Browse the repository at this point in the history
…pdate the cursor as things scroll underneath it

https://bugs.webkit.org/show_bug.cgi?id=273335
rdar://122347347

Reviewed by Tim Horton.

If you don't move the mouse after loading a page, we'll never update the cursor after scrolling via the trackpad.
This is because the web process never gets an updated mouse position if the only events we handle are wheel
events, which are handled entirely in the UI process (in the absence of JS wheel event handlers).

Fix by explicitly sending `setLastKnownMousePosition()` to EventHandler when we know that we're not
going to send the wheel events to the web process. Only do this on the `began` event, since we don't want to
flood the web process with new IPC when the position doesn't change (wheel gestures usually keep the same
position throughout).

Even when we did send wheel events to the web process, they would fail to update the last known mouse position,
so fix that in `EventHandler::handleWheelEventInternal()`.

I tried to write a test, but it was impossible to make it not flakey (it's attached to the radar).
However, the test revealed a race condition; the reload raced with the WebPage::m_pageScrolledHysteresis timer,
and sometimes this would cause `WebPage::pageDidScroll()` to get called right after we've constructed the new
ScrollView with a zero scroll position, which would get saved to the history item, and clobber the previously
saved scroll offset; this caused the scroll offset to get reset on reload. Fix by having `WebPage::didCommitLoad()`
call `cancel()` `m_pageScrolledHysteresis`, which needs to implemented in HysteresisActivity, with a test.

* Source/WebCore/PAL/pal/HysteresisActivity.h:
(PAL::HysteresisActivity::cancel):
* Source/WebCore/page/EventHandler.cpp:
(WebCore::EventHandler::handleMousePressEvent):
(WebCore::EventHandler::handleMouseDoubleClickEvent):
(WebCore::EventHandler::handleMouseMoveEvent):
(WebCore::EventHandler::handleMouseReleaseEvent):
(WebCore::EventHandler::handleMouseForceEvent):
(WebCore::EventHandler::handleWheelEventInternal):
(WebCore::EventHandler::setLastKnownMousePosition):
* Source/WebCore/page/EventHandler.h:
* Source/WebKit/UIProcess/WebPageProxy.cpp:
(WebKit::WebPageProxy::continueWheelEventHandling):
* Source/WebKit/WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::setLastKnownMousePosition):
(WebKit::WebPage::didCommitLoad):
* Source/WebKit/WebProcess/WebPage/WebPage.h:
* Source/WebKit/WebProcess/WebPage/WebPage.messages.in:
* Tools/TestWebKitAPI/Tests/WebCore/HysteresisActivityTests.cpp:
(TestWebKitAPI::TEST(HysteresisActivity, Cancelation)):

Canonical link: https://commits.webkit.org/278297@main
  • Loading branch information
smfr committed May 3, 2024
1 parent f2a9b39 commit e7ab50a
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 10 deletions.
7 changes: 7 additions & 0 deletions Source/WebCore/PAL/pal/HysteresisActivity.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ class HysteresisActivity {
m_timer.startOneShot(m_hysteresisSeconds);
}

void cancel()
{
m_active = false;
if (m_timer.isActive())
m_timer.stop();
}

void impulse()
{
if (m_active)
Expand Down
17 changes: 9 additions & 8 deletions Source/WebCore/page/EventHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1822,7 +1822,7 @@ HandleUserInputEventResult EventHandler::handleMousePressEvent(const PlatformMou

m_mousePressed = true;
m_capturesDragging = true;
setLastKnownMousePosition(platformMouseEvent);
setLastKnownMousePosition(platformMouseEvent.position(), platformMouseEvent.globalPosition());
m_mouseDownTimestamp = platformMouseEvent.timestamp();
#if ENABLE(DRAG_SUPPORT)
m_mouseDownMayStartDrag = false;
Expand Down Expand Up @@ -1964,7 +1964,7 @@ bool EventHandler::handleMouseDoubleClickEvent(const PlatformMouseEvent& platfor

// We get this instead of a second mouse-up
m_mousePressed = false;
setLastKnownMousePosition(platformMouseEvent);
setLastKnownMousePosition(platformMouseEvent.position(), platformMouseEvent.globalPosition());

constexpr OptionSet<HitTestRequest::Type> hitType { HitTestRequest::Type::Release, HitTestRequest::Type::DisallowUserAgentShadowContent };
MouseEventWithHitTestResults mouseEvent = prepareMouseEvent(hitType, platformMouseEvent);
Expand Down Expand Up @@ -2114,7 +2114,7 @@ HandleUserInputEventResult EventHandler::handleMouseMoveEvent(const PlatformMous
}
#endif

setLastKnownMousePosition(platformMouseEvent);
setLastKnownMousePosition(platformMouseEvent.position(), platformMouseEvent.globalPosition());

if (m_hoverTimer.isActive())
m_hoverTimer.stop();
Expand Down Expand Up @@ -2280,7 +2280,7 @@ HandleUserInputEventResult EventHandler::handleMouseReleaseEvent(const PlatformM
#endif

m_mousePressed = false;
setLastKnownMousePosition(platformMouseEvent);
setLastKnownMousePosition(platformMouseEvent.position(), platformMouseEvent.globalPosition());

if (m_svgPan) {
m_svgPan = false;
Expand Down Expand Up @@ -2354,7 +2354,7 @@ bool EventHandler::handleMouseForceEvent(const PlatformMouseEvent& event)
}
#endif

setLastKnownMousePosition(event);
setLastKnownMousePosition(event.position(), event.globalPosition());

OptionSet<HitTestRequest::Type> hitType { HitTestRequest::Type::DisallowUserAgentShadowContent };

Expand Down Expand Up @@ -3138,6 +3138,7 @@ HandleUserInputEventResult EventHandler::handleWheelEventInternal(const Platform
auto allowsScrollingState = SetForScope(m_currentWheelEventAllowsScrolling, processingSteps.contains(WheelEventProcessingSteps::SynchronousScrolling));

setFrameWasScrolledByUser();
setLastKnownMousePosition(event.position(), event.globalPosition());

if (m_frame->isMainFrame()) {
RefPtr page = m_frame->page();
Expand Down Expand Up @@ -5142,10 +5143,10 @@ HandleUserInputEventResult EventHandler::dispatchSyntheticTouchEventIfEnabled(co
}
#endif // ENABLE(TOUCH_EVENTS)

void EventHandler::setLastKnownMousePosition(const PlatformMouseEvent& event)
void EventHandler::setLastKnownMousePosition(IntPoint position, IntPoint globalPosition)
{
m_lastKnownMousePosition = event.position();
m_lastKnownMouseGlobalPosition = event.globalPosition();
m_lastKnownMousePosition = position;
m_lastKnownMouseGlobalPosition = globalPosition;
}

void EventHandler::setImmediateActionStage(ImmediateActionStage stage)
Expand Down
4 changes: 2 additions & 2 deletions Source/WebCore/page/EventHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ class EventHandler final : public CanMakeCheckedPtr<EventHandler> {
void defaultWheelEventHandler(Node*, WheelEvent&);
void wheelEventWasProcessedByMainThread(const PlatformWheelEvent&, OptionSet<EventHandling>);

WEBCORE_EXPORT void setLastKnownMousePosition(IntPoint position, IntPoint globalPosition);

bool handlePasteGlobalSelection(const PlatformMouseEvent&);

#if ENABLE(IOS_TOUCH_EVENTS) || ENABLE(IOS_GESTURE_EVENTS)
Expand Down Expand Up @@ -588,8 +590,6 @@ class EventHandler final : public CanMakeCheckedPtr<EventHandler> {
bool isKeyEventAllowedInFullScreen(const PlatformKeyboardEvent&) const;
#endif

void setLastKnownMousePosition(const PlatformMouseEvent&);

#if ENABLE(CURSOR_VISIBILITY)
void startAutoHideCursorTimer();
void cancelAutoHideCursorTimer();
Expand Down
5 changes: 5 additions & 0 deletions Source/WebKit/UIProcess/WebPageProxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3627,6 +3627,11 @@ void WebPageProxy::continueWheelEventHandling(const WebWheelEvent& wheelEvent, c
LOG_WITH_STREAM(WheelEvents, stream << "WebPageProxy::continueWheelEventHandling - " << result);

if (!result.needsMainThreadProcessing()) {
if (wheelEvent.phase() == WebWheelEvent::Phase::PhaseBegan) {
// When wheel events are handled entirely in the UI process, we still need to tell the web process where the mouse is for cursor updates.
sendToProcessContainingFrame(m_mainFrame->frameID(), Messages::WebPage::SetLastKnownMousePosition(m_mainFrame->frameID(), wheelEvent.position(), wheelEvent.globalPosition()));
}

wheelEventHandlingCompleted(result.wasHandled);
return;
}
Expand Down
12 changes: 12 additions & 0 deletions Source/WebKit/WebProcess/WebPage/WebPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3555,6 +3555,15 @@ void WebPage::mouseEvent(FrameIdentifier frameID, const WebMouseEvent& mouseEven
completionHandler(mouseEvent.type(), handled, std::nullopt);
}

void WebPage::setLastKnownMousePosition(WebCore::FrameIdentifier frameID, IntPoint eventPoint, IntPoint globalPoint)
{
auto* frame = WebProcess::singleton().webFrame(frameID);
if (!frame || !frame->coreLocalFrame() || !frame->coreLocalFrame()->view())
return;

frame->coreLocalFrame()->eventHandler().setLastKnownMousePosition(eventPoint, globalPoint);
}

void WebPage::flushDeferredDidReceiveMouseEvent()
{
if (auto info = std::exchange(m_deferredMouseEventCompletionHandler, std::nullopt))
Expand Down Expand Up @@ -7524,6 +7533,9 @@ void WebPage::didCommitLoad(WebFrame* frame)
scalePage(1, IntPoint());
}

// This timer can race with loading and clobber the scroll position saved on the current history item.
m_pageScrolledHysteresis.cancel();

m_didUpdateRenderingAfterCommittingLoad = false;

#if PLATFORM(IOS_FAMILY)
Expand Down
2 changes: 2 additions & 0 deletions Source/WebKit/WebProcess/WebPage/WebPage.h
Original file line number Diff line number Diff line change
Expand Up @@ -1896,6 +1896,8 @@ class WebPage : public API::ObjectImpl<API::Object::Type::BundlePage>, public IP
void mouseEvent(WebCore::FrameIdentifier, const WebMouseEvent&, std::optional<Vector<SandboxExtension::Handle>>&& sandboxExtensions, CompletionHandler<void(std::optional<WebEventType>, bool, std::optional<WebCore::RemoteUserInputEventData>)>&&);
void keyEvent(WebCore::FrameIdentifier, const WebKeyboardEvent&, CompletionHandler<void(std::optional<WebEventType>, bool)>&&);

void setLastKnownMousePosition(WebCore::FrameIdentifier, WebCore::IntPoint eventPoint, WebCore::IntPoint globalPoint);

#if ENABLE(IOS_TOUCH_EVENTS)
void touchEventSync(const WebTouchEvent&, CompletionHandler<void(bool)>&&);
void resetPotentialTapSecurityOrigin();
Expand Down
1 change: 1 addition & 0 deletions Source/WebKit/WebProcess/WebPage/WebPage.messages.in
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ messages -> WebPage LegacyReceiver {
ExecuteEditCommandWithCallback(String name, String argument) -> ()
KeyEvent(WebCore::FrameIdentifier frameID, WebKit::WebKeyboardEvent event) -> (std::optional<WebKit::WebEventType> eventType, bool handled)
MouseEvent(WebCore::FrameIdentifier frameID, WebKit::WebMouseEvent event, std::optional<Vector<WebKit::SandboxExtensionHandle>> sandboxExtensions) -> (std::optional<WebKit::WebEventType> eventType, bool handled, struct std::optional<WebCore::RemoteUserInputEventData> remoteUserInputEventData)
SetLastKnownMousePosition(WebCore::FrameIdentifier frameID, WebCore::IntPoint eventPoint, WebCore::IntPoint globalPoint);

#if ENABLE(NOTIFICATIONS)
ClearNotificationPermissionState()
Expand Down
40 changes: 40 additions & 0 deletions Tools/TestWebKitAPI/Tests/WebCore/HysteresisActivityTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,44 @@ TEST(HysteresisActivity, ActiveInCallback)
EXPECT_EQ(*callbackData.state, PAL::HysteresisState::Stopped);
}

TEST(HysteresisActivity, Cancelation)
{
struct CallbackData {
std::optional<PAL::HysteresisState> state;
PAL::HysteresisActivity* activity;
} callbackData;

PAL::HysteresisActivity activity([&callbackData](PAL::HysteresisState) {
callbackData.state = callbackData.activity->state();
if (endTestInCallback)
hysteresisTestDone = true;
}, hysteresisDuration);

callbackData.activity = &activity;

hysteresisTestDone = false;
endTestInCallback = false;

activity.start();
EXPECT_TRUE(callbackData.state);
EXPECT_EQ(*callbackData.state, PAL::HysteresisState::Started);

activity.cancel();
EXPECT_TRUE(callbackData.state);
// The callback does not get called.
EXPECT_EQ(*callbackData.state, PAL::HysteresisState::Started);

activity.impulse();
EXPECT_TRUE(callbackData.state);
EXPECT_EQ(*callbackData.state, PAL::HysteresisState::Started);

activity.cancel();
EXPECT_TRUE(callbackData.state);
// The callback does not get called.
EXPECT_EQ(*callbackData.state, PAL::HysteresisState::Started);

TestWebKitAPI::Util::runFor(2 * hysteresisDuration);
EXPECT_FALSE(hysteresisTestDone);
}

} // namespace TestWebKitAPI

0 comments on commit e7ab50a

Please sign in to comment.