← All issues

[AppKit Gestures] Selection-drag autoscroll to the window edge on macOS

17c30e3

WebKit separates text interaction on macOS into the legacy EventHandler path and the newer AppKit-gestures path driven by NSTextSelectionManager. In the AppKit path the UI process owns gesture recognition and sends extent-point updates to the web process over IPC; the web process owns scroll state and fires back scroll notifications. Autoscroll is driven by AutoscrollController's 50ms Selection timer inside the web process. On iOS, UIKit's UITextAutoscrolling protocol bridges these; AppKit has no equivalent.

Source/WebCore/page/EventHandler.cpp

void EventHandler::updateSelectionForMouseDrag()
{
if (!supportsSelectionUpdatesOnMouseDrag())
return;
 
+#if PLATFORM(COCOA)
+ // During a selection-drag, the UI process re-extends the selection as the page
+ // autoscrolls, so don't also re-extend here using the stale last-known mouse position.
+ if (m_isAutoscrolling)
+ return;
+#endif
-#endif // !PLATFORM(IOS_FAMILY)
+#endif // !PLATFORM(COCOA)

This commit implements selection-drag autoscroll on macOS for the AppKit-gestures path by reusing the existing iOS autoscroll machinery. It adds edge-band proximity detection in EventHandler (isPointNearSelectionAutoscrollEdge), a suppression guard in updateSelectionForMouseDrag, and a UI-process re-extension hook in WKTextSelectionController that fires on every scroll event while a drag is in-flight. The web process evaluates edge proximity on each incoming extent-point IPC and starts or cancels the autoscroll timer, then notifies the UI process on scroll so it can re-extend the selection into newly revealed content.

This introduces a new bidirectional cross-process feedback loop between the UI and web process for text selection, creating new IPC paths, coordinate-transform logic, and a timer-driven state machine that didn't exist on macOS before.

The m_isAutoscrolling guard silently drops all web-process-side selection updates while set — any path that sets it via non-selection autoscroll (e.g. a regular mouse-drag autoscroll on a scrollable container) would silently suppress selection extension. The cross-process feedback loop has no explicit iteration cap (UI sends extent → web scrolls → UI re-extends → repeat); the only brake is isPointNearSelectionAutoscrollEdge returning false, so a stale visible rect during rapid zoom or iframe resize may keep it looping. The zoomScale factor divides the edge hot-zone size and scroll speed with no visible clamp — verify behavior at extreme zoom (scale near zero produces an enormous hot-zone and the integer division may overflow). dragOriginInRootView is stored per-drag in the UI process and used as the hysteresis origin in the web process; if a second gesture begins or a drag is recycled mid-autoscroll, audit whether the origin is reset before being sent across IPC. Finally, the 50ms web-process timer and the UI-process reextendSelectionForAutoscrollIfNeeded are concurrent and IPC delivery order is not guaranteed — check whether the timer can fire after the cancel IPC is queued but before the web process processes it, leaving the timer active with no corresponding UI drag.