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 | |
43 | namespace WebCore { |
44 | |
45 | PointerCaptureController::PointerCaptureController(Page& page) |
46 | : m_page(page) |
47 | { |
48 | reset(); |
49 | } |
50 | |
51 | ExceptionOr<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 | |
81 | ExceptionOr<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 | |
104 | bool 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 | |
116 | void 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 | |
128 | void 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 | |
147 | void 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 | |
157 | void PointerCaptureController::touchWithIdentifierWasRemoved(PointerID pointerId) |
158 | { |
159 | m_activePointerIdsToCapturingData.remove(pointerId); |
160 | } |
161 | |
162 | bool PointerCaptureController::hasCancelledPointerEventForIdentifier(PointerID pointerId) |
163 | { |
164 | auto iterator = m_activePointerIdsToCapturingData.find(pointerId); |
165 | return iterator != m_activePointerIdsToCapturingData.end() && iterator->value.cancelled; |
166 | } |
167 | |
168 | bool 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) |
175 | void 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 | |
234 | RefPtr<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 | |
269 | void 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 | |
286 | void 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 | |
327 | void 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 | |
355 | void 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 | |
402 | void 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 | |