<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[287366] trunk</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/287366">287366</a></dd>
<dt>Author</dt> <dd>wenson_hsieh@apple.com</dd>
<dt>Date</dt> <dd>2021-12-22 11:12:30 -0800 (Wed, 22 Dec 2021)</dd>
</dl>

<h3>Log Message</h3>
<pre>[iOS] Scroll view pinch zoom gesture sometimes fails to recognize in WKWebView
https://bugs.webkit.org/show_bug.cgi?id=234584
rdar://84379650

Reviewed by Simon Fraser.

Source/WebKit:

WKWebView may get into a state where the pinch zoom gesture recognizer on its scroll view sometimes fails to
transition to Changed state (and invoke its action, which sets the zoomScale of the scroll view); this happens
because the scroll view pinch gesture requires the touch start deferring gesture recognizer (TSDG) to fail, but
it's possible for TSDG to remain stuck in Possible state over the duration of a gesture after beginning a touch
over content with either passive touch event listeners, or no touch event listeners.

In the case where the TSDG is stuck in Possible state, we observe the following sequence of events (let's
suppose the user is starting a touch over a passive touch event listener on a web page):

1.      The UITouch is delivered to the web touch event gesture recognizer, which fires the action
        (`-_webTouchEventsRecognized:`). We observe that we're over an async touch event handling region (i.e.
        passive or no touch event listeners), so we immediately "lift the gesture gate" by transitioning all
        deferring gesture recognizers to Failed state, (with the intent that they won't prevent native gestures
        from recognizing).

2.      A UITouch is then delivered to the TSDG in the same runloop as UIKit continues to deliver the touch
        event to all gestures in the `NSSet` of gesture recognizers on the window. Receiving the UITouch causes
        TSDG (which we already set to Failed state in step (1)) to internally reset and transition back to
        Possible state, underneath WebKit.

3.      TSDG is now in possible state after the gesture has begun, but we've already tried to unblock native
        gestures. When performing the second touch of the pinch zoom gesture, the pinch zoom gesture fails to
        commence because it's stuck waiting for TSDG to fail.

In the normal (working) scenario, step (2) happens before step (1); this ensures that TSDG is set to Failed
state and remains in Failed state over the course of the gesture, thereby preventing the bug from happening. The
order in which (1) and (2) happen is dependent on the order in which the web touch event gesture and TSDG are
iterated in UIKit's `NSSet` of gestures, which explains why this bug only reproduces some of the time.

This patch mitigates this by adding a mechanism to keep track of touch start deferrers that we've already
transitioned to Failed state during the course of a gesture, and uses this information to avoid adding failure
requirements to these deferring gestures that have already been "ungated". This ensures that even if the
deferring gesture resets to Possible state from underneath WebKit, we still avoid pushing out native gesture
recognition due to these deferring gestures.

As an aside, I initially attempted to avoid having TSDG transition back to Possible state altogether in this
scenario, but this doesn't seem to be avoidable (short of overriding the SPI method `-_resetGestureRecognizer`
and not calling the superclass, which does not seem to be supported behavior).

Test: fast/events/touch/ios/pinch-zoom-with-passive-touch-event-listeners.html

* UIProcess/ios/WKContentViewInteraction.h:
* UIProcess/ios/WKContentViewInteraction.mm:
(-[WKContentView _webTouchEventsRecognized:]):
(-[WKContentView _doneDeferringTouchStart:]):
(-[WKContentView deferringGestureRecognizer:shouldDeferOtherGestureRecognizer:]):

LayoutTests:

Add a test to exercise the fix. Note that this test will only fail without the fix *some* of the time, since it
depends entirely on the order in which two gestures appear in an Objective-C hash datastructure (see WebKit
ChangeLog for more details).

* fast/events/touch/ios/pinch-zoom-with-passive-touch-event-listeners-expected.txt: Added.
* fast/events/touch/ios/pinch-zoom-with-passive-touch-event-listeners.html: Added.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkLayoutTestsChangeLog">trunk/LayoutTests/ChangeLog</a></li>
<li><a href="#trunkSourceWebKitChangeLog">trunk/Source/WebKit/ChangeLog</a></li>
<li><a href="#trunkSourceWebKitUIProcessiosWKContentViewInteractionh">trunk/Source/WebKit/UIProcess/ios/WKContentViewInteraction.h</a></li>
<li><a href="#trunkSourceWebKitUIProcessiosWKContentViewInteractionmm">trunk/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkLayoutTestsfasteventstouchiospinchzoomwithpassivetoucheventlistenersexpectedtxt">trunk/LayoutTests/fast/events/touch/ios/pinch-zoom-with-passive-touch-event-listeners-expected.txt</a></li>
<li><a href="#trunkLayoutTestsfasteventstouchiospinchzoomwithpassivetoucheventlistenershtml">trunk/LayoutTests/fast/events/touch/ios/pinch-zoom-with-passive-touch-event-listeners.html</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkLayoutTestsChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/LayoutTests/ChangeLog (287365 => 287366)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/ChangeLog      2021-12-22 18:52:52 UTC (rev 287365)
+++ trunk/LayoutTests/ChangeLog 2021-12-22 19:12:30 UTC (rev 287366)
</span><span class="lines">@@ -1,3 +1,18 @@
</span><ins>+2021-12-22  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] Scroll view pinch zoom gesture sometimes fails to recognize in WKWebView
+        https://bugs.webkit.org/show_bug.cgi?id=234584
+        rdar://84379650
+
+        Reviewed by Simon Fraser.
+
+        Add a test to exercise the fix. Note that this test will only fail without the fix *some* of the time, since it
+        depends entirely on the order in which two gestures appear in an Objective-C hash datastructure (see WebKit
+        ChangeLog for more details).
+
+        * fast/events/touch/ios/pinch-zoom-with-passive-touch-event-listeners-expected.txt: Added.
+        * fast/events/touch/ios/pinch-zoom-with-passive-touch-event-listeners.html: Added.
+
</ins><span class="cx"> 2021-12-22  Antti Koivisto  <antti@apple.com>
</span><span class="cx"> 
</span><span class="cx">         [:has() pseudo-class] :has() selector invalidation issue with toggling :checked
</span></span></pre></div>
<a id="trunkLayoutTestsfasteventstouchiospinchzoomwithpassivetoucheventlistenersexpectedtxt"></a>
<div class="addfile"><h4>Added: trunk/LayoutTests/fast/events/touch/ios/pinch-zoom-with-passive-touch-event-listeners-expected.txt (0 => 287366)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/fast/events/touch/ios/pinch-zoom-with-passive-touch-event-listeners-expected.txt                               (rev 0)
+++ trunk/LayoutTests/fast/events/touch/ios/pinch-zoom-with-passive-touch-event-listeners-expected.txt  2021-12-22 19:12:30 UTC (rev 287366)
</span><span class="lines">@@ -0,0 +1,9 @@
</span><ins>+Verifies that the pinch zoom gesture is recognized in passive touch event regions. To manually run the test, pinch over the red square to zoom in.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+PASS Successfully zoomed in by pinching
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
</ins></span></pre></div>
<a id="trunkLayoutTestsfasteventstouchiospinchzoomwithpassivetoucheventlistenershtml"></a>
<div class="addfile"><h4>Added: trunk/LayoutTests/fast/events/touch/ios/pinch-zoom-with-passive-touch-event-listeners.html (0 => 287366)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/fast/events/touch/ios/pinch-zoom-with-passive-touch-event-listeners.html                               (rev 0)
+++ trunk/LayoutTests/fast/events/touch/ios/pinch-zoom-with-passive-touch-event-listeners.html  2021-12-22 19:12:30 UTC (rev 287366)
</span><span class="lines">@@ -0,0 +1,141 @@
</span><ins>+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true ] -->
+<html>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<head>
+<script src="../../../../resources/js-test.js"></script>
+<script src="../../../../resources/ui-helper.js"></script>
+<style>
+html, body {
+    width: 100%;
+    height: 100%;
+    margin: 0;
+}
+
+#box {
+    top: 100px;
+    left: 10px;
+    width: 300px;
+    height: 300px;
+    background-color: tomato;
+    position: absolute;
+}
+</style>
+</head>
+<body>
+<div id="box"></div>
+<pre id="description"></pre>
+<pre id="console"></pre>
+</body>
+<script>
+jsTestIsAsync = true;
+description("Verifies that the pinch zoom gesture is recognized in passive touch event regions. To manually run the test, pinch over the red square to zoom in.");
+document.getElementById("box").addEventListener("touchstart", () => { }, { passive: true });
+
+async function pinchZoom() {
+    await UIHelper.sendEventStream({
+        events: [
+            {
+                interpolate : "linear",
+                timestep : 0.01,
+                coordinateSpace : "content",
+                startEvent : {
+                    inputType : "hand",
+                    timeOffset : 0,
+                    touches : [{ inputType : "finger", phase : "began", id : 1, x : 140, y : 200, pressure : 0 }]
+                },
+                endEvent : {
+                    inputType : "hand",
+                    timeOffset : 0.01,
+                    touches : [{ inputType : "finger", phase : "began", id : 1, x : 140, y : 200, pressure : 0 }]
+                }
+            },
+            {
+                interpolate : "linear",
+                timestep : 0.01,
+                coordinateSpace : "content",
+                startEvent : {
+                    inputType : "hand",
+                    timeOffset : 0.01,
+                    touches : [{ inputType : "finger", phase : "moved", id : 1, x : 140, y : 200, pressure : 0 }]
+                },
+                endEvent : {
+                    inputType : "hand",
+                    timeOffset : 0.49,
+                    touches : [{ inputType : "finger", phase : "moved", id : 1, x : 140, y : 100, pressure : 0 }]
+                }
+            },
+            {
+                interpolate : "linear",
+                timestep : 0.01,
+                coordinateSpace : "content",
+                startEvent : {
+                    inputType : "hand",
+                    timeOffset : 0.49,
+                    touches : [
+                        { inputType : "finger", phase : "stationary", id : 1, x : 140, y : 100, pressure : 0 },
+                        { inputType : "finger", phase : "began", id : 2, x : 140, y : 200, pressure : 0 }
+                    ]
+                },
+                endEvent : {
+                    inputType : "hand",
+                    timeOffset : 0.5,
+                    touches : [
+                        { inputType : "finger", phase : "stationary", id : 1, x : 140, y : 100, pressure : 0 },
+                        { inputType : "finger", phase : "began", id : 2, x : 140, y : 200, pressure : 0 }
+                    ]
+                }
+            },
+            {
+                interpolate : "linear",
+                timestep : 0.01,
+                coordinateSpace : "content",
+                startEvent : {
+                    inputType : "hand",
+                    timeOffset : 0.5,
+                    touches : [
+                        { inputType : "finger", phase : "moved", id : 1, x : 140, y : 100, pressure : 0 },
+                        { inputType : "finger", phase : "moved", id : 2, x : 140, y : 200, pressure : 0 }
+                    ]
+                },
+                endEvent : {
+                    inputType : "hand",
+                    timeOffset : 0.99,
+                    touches : [
+                        { inputType : "finger", phase : "moved", id : 1, x : 140, y : 20, pressure : 0 },
+                        { inputType : "finger", phase : "moved", id : 2, x : 140, y : 300, pressure : 0 }
+                    ]
+                }
+            },
+            {
+                interpolate : "linear",
+                timestep : 0.01,
+                coordinateSpace : "content",
+                startEvent : {
+                    inputType : "hand",
+                    timeOffset : 0.99,
+                    touches : [
+                        { inputType : "finger", phase : "ended", id : 1, x : 140, y : 20, pressure : 0 },
+                        { inputType : "finger", phase : "ended", id : 2, x : 140, y : 300, pressure : 0 }
+                    ]
+                },
+                endEvent : {
+                    inputType : "hand",
+                    timeOffset : 1,
+                    touches : [
+                        { inputType : "finger", phase : "ended", id : 1, x : 140, y : 20, pressure : 0 },
+                        { inputType : "finger", phase : "ended", id : 2, x : 140, y : 300, pressure : 0 }
+                    ]
+                }
+            }
+        ]
+    });
+}
+
+addEventListener("load", async () => {
+    while (visualViewport.scale <= 1)
+        await pinchZoom();
+    testPassed("Successfully zoomed in by pinching");
+    finishJSTest();
+});
+</script>
+</html>
</ins><span class="cx">\ No newline at end of file
</span></span></pre></div>
<a id="trunkSourceWebKitChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebKit/ChangeLog (287365 => 287366)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebKit/ChangeLog    2021-12-22 18:52:52 UTC (rev 287365)
+++ trunk/Source/WebKit/ChangeLog       2021-12-22 19:12:30 UTC (rev 287366)
</span><span class="lines">@@ -1,3 +1,58 @@
</span><ins>+2021-12-22  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] Scroll view pinch zoom gesture sometimes fails to recognize in WKWebView
+        https://bugs.webkit.org/show_bug.cgi?id=234584
+        rdar://84379650
+
+        Reviewed by Simon Fraser.
+
+        WKWebView may get into a state where the pinch zoom gesture recognizer on its scroll view sometimes fails to
+        transition to Changed state (and invoke its action, which sets the zoomScale of the scroll view); this happens
+        because the scroll view pinch gesture requires the touch start deferring gesture recognizer (TSDG) to fail, but
+        it's possible for TSDG to remain stuck in Possible state over the duration of a gesture after beginning a touch
+        over content with either passive touch event listeners, or no touch event listeners.
+
+        In the case where the TSDG is stuck in Possible state, we observe the following sequence of events (let's
+        suppose the user is starting a touch over a passive touch event listener on a web page):
+
+        1.      The UITouch is delivered to the web touch event gesture recognizer, which fires the action
+                (`-_webTouchEventsRecognized:`). We observe that we're over an async touch event handling region (i.e.
+                passive or no touch event listeners), so we immediately "lift the gesture gate" by transitioning all
+                deferring gesture recognizers to Failed state, (with the intent that they won't prevent native gestures
+                from recognizing).
+
+        2.      A UITouch is then delivered to the TSDG in the same runloop as UIKit continues to deliver the touch
+                event to all gestures in the `NSSet` of gesture recognizers on the window. Receiving the UITouch causes
+                TSDG (which we already set to Failed state in step (1)) to internally reset and transition back to
+                Possible state, underneath WebKit.
+
+        3.      TSDG is now in possible state after the gesture has begun, but we've already tried to unblock native
+                gestures. When performing the second touch of the pinch zoom gesture, the pinch zoom gesture fails to
+                commence because it's stuck waiting for TSDG to fail.
+
+        In the normal (working) scenario, step (2) happens before step (1); this ensures that TSDG is set to Failed
+        state and remains in Failed state over the course of the gesture, thereby preventing the bug from happening. The
+        order in which (1) and (2) happen is dependent on the order in which the web touch event gesture and TSDG are
+        iterated in UIKit's `NSSet` of gestures, which explains why this bug only reproduces some of the time.
+
+        This patch mitigates this by adding a mechanism to keep track of touch start deferrers that we've already
+        transitioned to Failed state during the course of a gesture, and uses this information to avoid adding failure
+        requirements to these deferring gestures that have already been "ungated". This ensures that even if the
+        deferring gesture resets to Possible state from underneath WebKit, we still avoid pushing out native gesture
+        recognition due to these deferring gestures.
+
+        As an aside, I initially attempted to avoid having TSDG transition back to Possible state altogether in this
+        scenario, but this doesn't seem to be avoidable (short of overriding the SPI method `-_resetGestureRecognizer`
+        and not calling the superclass, which does not seem to be supported behavior).
+
+        Test: fast/events/touch/ios/pinch-zoom-with-passive-touch-event-listeners.html
+
+        * UIProcess/ios/WKContentViewInteraction.h:
+        * UIProcess/ios/WKContentViewInteraction.mm:
+        (-[WKContentView _webTouchEventsRecognized:]):
+        (-[WKContentView _doneDeferringTouchStart:]):
+        (-[WKContentView deferringGestureRecognizer:shouldDeferOtherGestureRecognizer:]):
+
</ins><span class="cx"> 2021-12-22  Simon Fraser  <simon.fraser@apple.com>
</span><span class="cx"> 
</span><span class="cx">         Preferences that read from NSUserDefaults need to be initialied from platformInitializeStore()
</span></span></pre></div>
<a id="trunkSourceWebKitUIProcessiosWKContentViewInteractionh"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebKit/UIProcess/ios/WKContentViewInteraction.h (287365 => 287366)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebKit/UIProcess/ios/WKContentViewInteraction.h     2021-12-22 18:52:52 UTC (rev 287365)
+++ trunk/Source/WebKit/UIProcess/ios/WKContentViewInteraction.h        2021-12-22 19:12:30 UTC (rev 287366)
</span><span class="lines">@@ -65,6 +65,7 @@
</span><span class="cx"> #import <wtf/CompletionHandler.h>
</span><span class="cx"> #import <wtf/Forward.h>
</span><span class="cx"> #import <wtf/Function.h>
</span><ins>+#import <wtf/HashSet.h>
</ins><span class="cx"> #import <wtf/ObjectIdentifier.h>
</span><span class="cx"> #import <wtf/OptionSet.h>
</span><span class="cx"> #import <wtf/Vector.h>
</span><span class="lines">@@ -270,6 +271,7 @@
</span><span class="cx">     RetainPtr<WKDeferringGestureRecognizer> _touchEndDeferringGestureRecognizerForImmediatelyResettableGestures;
</span><span class="cx">     RetainPtr<WKDeferringGestureRecognizer> _touchEndDeferringGestureRecognizerForDelayedResettableGestures;
</span><span class="cx">     RetainPtr<WKDeferringGestureRecognizer> _touchEndDeferringGestureRecognizerForSyntheticTapGestures;
</span><ins>+    std::optional<HashSet<RetainPtr<WKDeferringGestureRecognizer>>> _failedTouchStartDeferringGestures;
</ins><span class="cx"> #if ENABLE(IMAGE_ANALYSIS)
</span><span class="cx">     RetainPtr<WKDeferringGestureRecognizer> _imageAnalysisDeferringGestureRecognizer;
</span><span class="cx"> #endif
</span></span></pre></div>
<a id="trunkSourceWebKitUIProcessiosWKContentViewInteractionmm"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm (287365 => 287366)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm    2021-12-22 18:52:52 UTC (rev 287365)
+++ trunk/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm       2021-12-22 19:12:30 UTC (rev 287366)
</span><span class="lines">@@ -1726,6 +1726,9 @@
</span><span class="cx"> 
</span><span class="cx">     _lastInteractionLocation = lastTouchEvent->locationInDocumentCoordinates;
</span><span class="cx">     if (lastTouchEvent->type == UIWebTouchEventTouchBegin) {
</span><ins>+        if (!_failedTouchStartDeferringGestures)
+            _failedTouchStartDeferringGestures = { { } };
+
</ins><span class="cx">         [self _handleDOMPasteRequestWithResult:WebCore::DOMPasteAccessResponse::DeniedForGesture];
</span><span class="cx">         _layerTreeTransactionIdAtLastInteractionStart = downcast<WebKit::RemoteLayerTreeDrawingAreaProxy>(*_page->drawingArea()).lastCommittedLayerTreeTransactionID();
</span><span class="cx"> 
</span><span class="lines">@@ -1771,6 +1774,8 @@
</span><span class="cx"> 
</span><span class="cx">         if (!_page->isHandlingPreventableTouchEnd())
</span><span class="cx">             stopDeferringNativeGesturesIfNeeded(self._touchEndDeferringGestures);
</span><ins>+
+        _failedTouchStartDeferringGestures = std::nullopt;
</ins><span class="cx">     }
</span><span class="cx"> #endif // ENABLE(TOUCH_EVENTS)
</span><span class="cx"> }
</span><span class="lines">@@ -2016,8 +2021,11 @@
</span><span class="cx"> 
</span><span class="cx"> - (void)_doneDeferringTouchStart:(BOOL)preventNativeGestures
</span><span class="cx"> {
</span><del>-    for (WKDeferringGestureRecognizer *gesture in self._touchStartDeferringGestures)
-        [gesture endDeferral:preventNativeGestures ? WebKit::ShouldPreventGestures::Yes : WebKit::ShouldPreventGestures::No];
</del><ins>+    for (WKDeferringGestureRecognizer *gestureRecognizer in self._touchStartDeferringGestures) {
+        [gestureRecognizer endDeferral:preventNativeGestures ? WebKit::ShouldPreventGestures::Yes : WebKit::ShouldPreventGestures::No];
+        if (_failedTouchStartDeferringGestures && !preventNativeGestures)
+            _failedTouchStartDeferringGestures->add(gestureRecognizer);
+    }
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> - (void)_doneDeferringTouchEnd:(BOOL)preventNativeGestures
</span><span class="lines">@@ -8002,6 +8010,15 @@
</span><span class="cx">     if ([self _touchEventsMustRequireGestureRecognizerToFail:gestureRecognizer])
</span><span class="cx">         return NO;
</span><span class="cx"> 
</span><ins>+    if (_failedTouchStartDeferringGestures && _failedTouchStartDeferringGestures->contains(deferringGestureRecognizer)
+        && deferringGestureRecognizer.state == UIGestureRecognizerStatePossible) {
+        // This deferring gesture no longer has an oppportunity to defer native gestures (either because the touch region did not have any
+        // active touch event listeners, or because any active touch event listeners on the page have already executed, and did not prevent
+        // default). UIKit may have already reset the gesture to Possible state underneath us, in which case we still need to treat it as
+        // if it has already failed; otherwise, we will incorrectly defer other gestures in the web view, such as scroll view pinching.
+        return NO;
+    }
+
</ins><span class="cx">     auto webView = _webView.getAutoreleased();
</span><span class="cx">     auto view = gestureRecognizer.view;
</span><span class="cx">     BOOL gestureIsInstalledOnOrUnderWebView = NO;
</span></span></pre>
</div>
</div>

</body>
</html>