1/*
2 * Copyright (C) 2019 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16 * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
17 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
20 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23 */
24
25#include "config.h"
26#include "PointerCaptureController.h"
27
28#if ENABLE(POINTER_EVENTS)
29
30#include "Document.h"
31#include "Element.h"
32#include "EventHandler.h"
33#include "EventNames.h"
34#include "EventTarget.h"
35#include "Page.h"
36#include "PointerEvent.h"
37#include <wtf/CheckedArithmetic.h>
38
39#if ENABLE(POINTER_LOCK)
40#include "PointerLockController.h"
41#endif
42
43namespace WebCore {
44
45PointerCaptureController::PointerCaptureController(Page& page)
46 : m_page(page)
47{
48 reset();
49}
50
51ExceptionOr<void> PointerCaptureController::setPointerCapture(Element* capturingTarget, PointerID pointerId)
52{
53 // https://w3c.github.io/pointerevents/#setting-pointer-capture
54
55 // 1. If the pointerId provided as the method's argument does not match any of the active pointers, then throw a DOMException with the name NotFoundError.
56 auto iterator = m_activePointerIdsToCapturingData.find(pointerId);
57 if (iterator == m_activePointerIdsToCapturingData.end())
58 return Exception { NotFoundError };
59
60 // 2. If the Element on which this method is invoked is not connected, throw an exception with the name InvalidStateError.
61 if (!capturingTarget->isConnected())
62 return Exception { InvalidStateError };
63
64#if ENABLE(POINTER_LOCK)
65 // 3. If this method is invoked while the document has a locked element, throw an exception with the name InvalidStateError.
66 if (auto* page = capturingTarget->document().page()) {
67 if (page->pointerLockController().isLocked())
68 return Exception { InvalidStateError };
69 }
70#endif
71
72 // 4. If the pointer is not in the active buttons state, then terminate these steps.
73 // 5. For the specified pointerId, set the pending pointer capture target override to the Element on which this method was invoked.
74 auto& capturingData = iterator->value;
75 if (capturingData.pointerIsPressed)
76 capturingData.pendingTargetOverride = capturingTarget;
77
78 return { };
79}
80
81ExceptionOr<void> PointerCaptureController::releasePointerCapture(Element* capturingTarget, PointerID pointerId)
82{
83 // https://w3c.github.io/pointerevents/#releasing-pointer-capture
84
85 // Pointer capture is released on an element explicitly by calling the element.releasePointerCapture(pointerId) method.
86 // When this method is called, a user agent MUST run the following steps:
87
88 // 1. If the pointerId provided as the method's argument does not match any of the active pointers and these steps are not
89 // being invoked as a result of the implicit release of pointer capture, then throw a DOMException with the name NotFoundError.
90 auto iterator = m_activePointerIdsToCapturingData.find(pointerId);
91 if (iterator == m_activePointerIdsToCapturingData.end())
92 return Exception { NotFoundError };
93
94 // 2. If hasPointerCapture is false for the Element with the specified pointerId, then terminate these steps.
95 if (!hasPointerCapture(capturingTarget, pointerId))
96 return { };
97
98 // 3. For the specified pointerId, clear the pending pointer capture target override, if set.
99 iterator->value.pendingTargetOverride = nullptr;
100
101 return { };
102}
103
104bool PointerCaptureController::hasPointerCapture(Element* capturingTarget, PointerID pointerId)
105{
106 // https://w3c.github.io/pointerevents/#dom-element-haspointercapture
107
108 // Indicates whether the element on which this method is invoked has pointer capture for the pointer identified by the argument pointerId.
109 // In particular, returns true if the pending pointer capture target override for pointerId is set to the element on which this method is
110 // invoked, and false otherwise.
111
112 auto iterator = m_activePointerIdsToCapturingData.find(pointerId);
113 return iterator != m_activePointerIdsToCapturingData.end() && iterator->value.pendingTargetOverride == capturingTarget;
114}
115
116void PointerCaptureController::pointerLockWasApplied()
117{
118 // https://w3c.github.io/pointerevents/#implicit-release-of-pointer-capture
119
120 // When a pointer lock is successfully applied on an element, a user agent MUST run the steps as if the releasePointerCapture()
121 // method has been called if any element is set to be captured or pending to be captured.
122 for (auto& capturingData : m_activePointerIdsToCapturingData.values()) {
123 capturingData.pendingTargetOverride = nullptr;
124 capturingData.targetOverride = nullptr;
125 }
126}
127
128void PointerCaptureController::elementWasRemoved(Element& element)
129{
130 for (auto& keyAndValue : m_activePointerIdsToCapturingData) {
131 auto& capturingData = keyAndValue.value;
132 if (capturingData.pendingTargetOverride == &element || capturingData.targetOverride == &element) {
133 // https://w3c.github.io/pointerevents/#implicit-release-of-pointer-capture
134 // When the pointer capture target override is no longer connected, the pending pointer capture target override and pointer capture target
135 // override nodes SHOULD be cleared and also a PointerEvent named lostpointercapture corresponding to the captured pointer SHOULD be fired
136 // at the document.
137 ASSERT(WTF::isInBounds<PointerID>(keyAndValue.key));
138 auto pointerId = static_cast<PointerID>(keyAndValue.key);
139 auto pointerType = capturingData.pointerType;
140 releasePointerCapture(&element, pointerId);
141 element.document().enqueueDocumentEvent(PointerEvent::create(eventNames().lostpointercaptureEvent, pointerId, pointerType));
142 return;
143 }
144 }
145}
146
147void PointerCaptureController::reset()
148{
149 m_activePointerIdsToCapturingData.clear();
150#if !ENABLE(TOUCH_EVENTS)
151 CapturingData capturingData;
152 capturingData.pointerType = PointerEvent::mousePointerType();
153 m_activePointerIdsToCapturingData.add(mousePointerID, capturingData);
154#endif
155}
156
157void PointerCaptureController::touchWithIdentifierWasRemoved(PointerID pointerId)
158{
159 m_activePointerIdsToCapturingData.remove(pointerId);
160}
161
162bool PointerCaptureController::hasCancelledPointerEventForIdentifier(PointerID pointerId)
163{
164 auto iterator = m_activePointerIdsToCapturingData.find(pointerId);
165 return iterator != m_activePointerIdsToCapturingData.end() && iterator->value.cancelled;
166}
167
168bool PointerCaptureController::preventsCompatibilityMouseEventsForIdentifier(PointerID pointerId)
169{
170 auto iterator = m_activePointerIdsToCapturingData.find(pointerId);
171 return iterator != m_activePointerIdsToCapturingData.end() && iterator->value.preventsCompatibilityMouseEvents;
172}
173
174#if ENABLE(TOUCH_EVENTS) && PLATFORM(IOS_FAMILY)
175void PointerCaptureController::dispatchEventForTouchAtIndex(EventTarget& target, const PlatformTouchEvent& platformTouchEvent, unsigned index, bool isPrimary, WindowProxy& view)
176{
177 auto dispatchEvent = [&](const String& type) {
178 target.dispatchEvent(PointerEvent::create(type, platformTouchEvent, index, isPrimary, view));
179 };
180
181 auto dispatchEnterOrLeaveEvent = [&](const String& type) {
182 if (!is<Element>(&target))
183 return;
184
185 auto* targetElement = &downcast<Element>(target);
186
187 bool hasCapturingListenerInHierarchy = false;
188 for (ContainerNode* curr = targetElement; curr; curr = curr->parentInComposedTree()) {
189 if (curr->hasCapturingEventListeners(type)) {
190 hasCapturingListenerInHierarchy = true;
191 break;
192 }
193 }
194
195 Vector<Ref<Element>, 32> targetChain;
196 for (Element* element = targetElement; element; element = element->parentElementInComposedTree()) {
197 if (hasCapturingListenerInHierarchy || element->hasEventListeners(type))
198 targetChain.append(*element);
199 }
200
201 if (type == eventNames().pointerenterEvent) {
202 for (auto& element : WTF::makeReversedRange(targetChain))
203 element->dispatchEvent(PointerEvent::create(type, platformTouchEvent, index, isPrimary, view));
204 } else {
205 for (auto& element : targetChain)
206 element->dispatchEvent(PointerEvent::create(type, platformTouchEvent, index, isPrimary, view));
207 }
208 };
209
210 auto pointerEvent = PointerEvent::create(platformTouchEvent, index, isPrimary, view);
211
212 if (pointerEvent->type() == eventNames().pointerdownEvent) {
213 // https://w3c.github.io/pointerevents/#the-pointerdown-event
214 // For input devices that do not support hover, a user agent MUST also fire a pointer event named pointerover followed by a pointer event named
215 // pointerenter prior to dispatching the pointerdown event.
216 dispatchEvent(eventNames().pointeroverEvent);
217 dispatchEnterOrLeaveEvent(eventNames().pointerenterEvent);
218 }
219
220 pointerEventWillBeDispatched(pointerEvent, &target);
221 target.dispatchEvent(pointerEvent);
222 pointerEventWasDispatched(pointerEvent);
223
224 if (pointerEvent->type() == eventNames().pointerupEvent) {
225 // https://w3c.github.io/pointerevents/#the-pointerup-event
226 // For input devices that do not support hover, a user agent MUST also fire a pointer event named pointerout followed by a
227 // pointer event named pointerleave after dispatching the pointerup event.
228 dispatchEvent(eventNames().pointeroutEvent);
229 dispatchEnterOrLeaveEvent(eventNames().pointerleaveEvent);
230 }
231}
232#endif
233
234RefPtr<PointerEvent> PointerCaptureController::pointerEventForMouseEvent(const MouseEvent& mouseEvent)
235{
236 const auto& type = mouseEvent.type();
237 const auto& names = eventNames();
238
239 auto iterator = m_activePointerIdsToCapturingData.find(mousePointerID);
240 ASSERT(iterator != m_activePointerIdsToCapturingData.end());
241 auto& capturingData = iterator->value;
242
243 short newButton = mouseEvent.button();
244 short button = (type == names.mousemoveEvent && newButton == capturingData.previousMouseButton) ? -1 : newButton;
245
246 // https://w3c.github.io/pointerevents/#chorded-button-interactions
247 // Some pointer devices, such as mouse or pen, support multiple buttons. In the Mouse Event model, each button
248 // press produces a mousedown and mouseup event. To better abstract this hardware difference and simplify
249 // cross-device input authoring, Pointer Events do not fire overlapping pointerdown and pointerup events
250 // for chorded button presses (depressing an additional button while another button on the pointer device is
251 // already depressed).
252 if (type == names.mousedownEvent || type == names.mouseupEvent) {
253 // We're already active and getting another mousedown, this means that we should dispatch
254 // a pointermove event and let the button state show the newly depressed button.
255 if (type == names.mousedownEvent && capturingData.pointerIsPressed)
256 return PointerEvent::create(names.pointermoveEvent, button, mouseEvent);
257
258 // We're active and the mouseup still has some pressed button, this means we should dispatch
259 // a pointermove event.
260 if (type == names.mouseupEvent && capturingData.pointerIsPressed && mouseEvent.buttons() > 0)
261 return PointerEvent::create(names.pointermoveEvent, button, mouseEvent);
262 }
263
264 capturingData.previousMouseButton = newButton;
265
266 return PointerEvent::create(button, mouseEvent);
267}
268
269void PointerCaptureController::dispatchEvent(PointerEvent& event, EventTarget* target)
270{
271 auto iterator = m_activePointerIdsToCapturingData.find(event.pointerId());
272 if (iterator != m_activePointerIdsToCapturingData.end()) {
273 auto& capturingData = iterator->value;
274 if (capturingData.pendingTargetOverride && capturingData.targetOverride)
275 target = capturingData.targetOverride.get();
276 }
277
278 if (!target || event.target())
279 return;
280
281 pointerEventWillBeDispatched(event, target);
282 target->dispatchEvent(event);
283 pointerEventWasDispatched(event);
284}
285
286void PointerCaptureController::pointerEventWillBeDispatched(const PointerEvent& event, EventTarget* target)
287{
288 if (!is<Element>(target))
289 return;
290
291 bool isPointerdown = event.type() == eventNames().pointerdownEvent;
292 bool isPointerup = event.type() == eventNames().pointerupEvent;
293 if (!isPointerdown && !isPointerup)
294 return;
295
296 auto pointerId = event.pointerId();
297
298 if (event.pointerType() == PointerEvent::mousePointerType()) {
299 auto iterator = m_activePointerIdsToCapturingData.find(pointerId);
300 if (iterator != m_activePointerIdsToCapturingData.end())
301 iterator->value.pointerIsPressed = isPointerdown;
302 return;
303 }
304
305 if (!isPointerdown)
306 return;
307
308 // https://w3c.github.io/pointerevents/#implicit-pointer-capture
309
310 // Some input devices (such as touchscreens) implement a "direct manipulation" metaphor where a pointer is intended to act primarily on the UI
311 // element it became active upon (providing a physical illusion of direct contact, instead of indirect contact via a cursor that conceptually
312 // floats above the UI). Such devices are identified by the InputDeviceCapabilities.pointerMovementScrolls property and should have "implicit
313 // pointer capture" behavior as follows.
314
315 // Direct manipulation devices should behave exactly as if setPointerCapture was called on the target element just before the invocation of any
316 // pointerdown listeners. The hasPointerCapture API may be used (eg. within any pointerdown listener) to determine whether this has occurred. If
317 // releasePointerCapture is not called for the pointer before the next pointer event is fired, then a gotpointercapture event will be dispatched
318 // to the target (as normal) indicating that capture is active.
319
320 CapturingData capturingData;
321 capturingData.pointerType = event.pointerType();
322 capturingData.pointerIsPressed = true;
323 m_activePointerIdsToCapturingData.set(pointerId, capturingData);
324 setPointerCapture(downcast<Element>(target), pointerId);
325}
326
327void PointerCaptureController::pointerEventWasDispatched(const PointerEvent& event)
328{
329 auto iterator = m_activePointerIdsToCapturingData.find(event.pointerId());
330 if (iterator != m_activePointerIdsToCapturingData.end()) {
331 auto& capturingData = iterator->value;
332 capturingData.isPrimary = event.isPrimary();
333
334 // Immediately after firing the pointerup or pointercancel events, a user agent MUST clear the pending pointer capture target
335 // override for the pointerId of the pointerup or pointercancel event that was just dispatched, and then run Process Pending
336 // Pointer Capture steps to fire lostpointercapture if necessary.
337 // https://w3c.github.io/pointerevents/#implicit-release-of-pointer-capture
338 if (event.type() == eventNames().pointerupEvent)
339 capturingData.pendingTargetOverride = nullptr;
340
341 // If a mouse pointer has moved while it isn't pressed, make sure we reset the preventsCompatibilityMouseEvents flag since
342 // we could otherwise prevent compatibility mouse events while those are only supposed to be prevented while the pointer is pressed.
343 if (event.type() == eventNames().pointermoveEvent && capturingData.pointerType == PointerEvent::mousePointerType() && !capturingData.pointerIsPressed)
344 capturingData.preventsCompatibilityMouseEvents = false;
345
346 // If the pointer event dispatched was pointerdown and the event was canceled, then set the PREVENT MOUSE EVENT flag for this pointerType.
347 // https://www.w3.org/TR/pointerevents/#mapping-for-devices-that-support-hover
348 if (event.type() == eventNames().pointerdownEvent)
349 capturingData.preventsCompatibilityMouseEvents = event.defaultPrevented();
350 }
351
352 processPendingPointerCapture(event);
353}
354
355void PointerCaptureController::cancelPointer(PointerID pointerId, const IntPoint& documentPoint)
356{
357 // https://w3c.github.io/pointerevents/#the-pointercancel-event
358
359 // A user agent MUST fire a pointer event named pointercancel in the following circumstances:
360 //
361 // The user agent has determined that a pointer is unlikely to continue to produce events (for example, because of a hardware event).
362 // After having fired the pointerdown event, if the pointer is subsequently used to manipulate the page viewport (e.g. panning or zooming).
363 // Immediately before drag operation starts [HTML], for the pointer that caused the drag operation.
364 // After firing the pointercancel event, a user agent MUST also fire a pointer event named pointerout followed by firing a pointer event named pointerleave.
365
366 // https://w3c.github.io/pointerevents/#implicit-release-of-pointer-capture
367
368 // Immediately after firing the pointerup or pointercancel events, a user agent MUST clear the pending pointer capture target
369 // override for the pointerId of the pointerup or pointercancel event that was just dispatched, and then run Process Pending
370 // Pointer Capture steps to fire lostpointercapture if necessary. After running Process Pending Pointer Capture steps, if the
371 // pointer supports hover, user agent MUST also send corresponding boundary events necessary to reflect the current position of
372 // the pointer with no capture.
373
374 auto iterator = m_activePointerIdsToCapturingData.find(pointerId);
375 if (iterator == m_activePointerIdsToCapturingData.end())
376 return;
377
378 auto& capturingData = iterator->value;
379 if (capturingData.cancelled)
380 return;
381
382 capturingData.pendingTargetOverride = nullptr;
383 capturingData.cancelled = true;
384
385 auto& target = capturingData.targetOverride;
386 if (!target)
387 target = m_page.mainFrame().eventHandler().hitTestResultAtPoint(documentPoint, HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::DisallowUserAgentShadowContent | HitTestRequest::AllowChildFrameContent).innerNonSharedElement();
388
389 if (!target)
390 return;
391
392 // After firing the pointercancel event, a user agent MUST also fire a pointer event named pointerout
393 // followed by firing a pointer event named pointerleave.
394 auto isPrimary = capturingData.isPrimary ? PointerEvent::IsPrimary::Yes : PointerEvent::IsPrimary::No;
395 auto cancelEvent = PointerEvent::create(eventNames().pointercancelEvent, pointerId, capturingData.pointerType, isPrimary);
396 target->dispatchEvent(cancelEvent);
397 target->dispatchEvent(PointerEvent::create(eventNames().pointeroutEvent, pointerId, capturingData.pointerType, isPrimary));
398 target->dispatchEvent(PointerEvent::create(eventNames().pointerleaveEvent, pointerId, capturingData.pointerType, isPrimary));
399 processPendingPointerCapture(WTFMove(cancelEvent));
400}
401
402void PointerCaptureController::processPendingPointerCapture(const PointerEvent& event)
403{
404 // https://w3c.github.io/pointerevents/#process-pending-pointer-capture
405
406 auto iterator = m_activePointerIdsToCapturingData.find(event.pointerId());
407 if (iterator == m_activePointerIdsToCapturingData.end())
408 return;
409
410 auto& capturingData = iterator->value;
411
412 // 1. If the pointer capture target override for this pointer is set and is not equal to the pending pointer capture target override,
413 // then fire a pointer event named lostpointercapture at the pointer capture target override node.
414 if (capturingData.targetOverride && capturingData.targetOverride != capturingData.pendingTargetOverride)
415 capturingData.targetOverride->dispatchEvent(PointerEvent::createForPointerCapture(eventNames().lostpointercaptureEvent, event));
416
417 // 2. If the pending pointer capture target override for this pointer is set and is not equal to the pointer capture target override,
418 // then fire a pointer event named gotpointercapture at the pending pointer capture target override.
419 if (capturingData.pendingTargetOverride && capturingData.targetOverride != capturingData.pendingTargetOverride)
420 capturingData.pendingTargetOverride->dispatchEvent(PointerEvent::createForPointerCapture(eventNames().gotpointercaptureEvent, event));
421
422 // 3. Set the pointer capture target override to the pending pointer capture target override, if set. Otherwise, clear the pointer
423 // capture target override.
424 capturingData.targetOverride = capturingData.pendingTargetOverride;
425}
426
427} // namespace WebCore
428
429#endif // ENABLE(POINTER_EVENTS)
430