File: PointerLockController.cpp

package info (click to toggle)
webkit2gtk 2.48.5-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 429,764 kB
  • sloc: cpp: 3,697,587; javascript: 194,444; ansic: 169,997; python: 46,499; asm: 19,295; ruby: 18,528; perl: 16,602; xml: 4,650; yacc: 2,360; sh: 2,098; java: 1,993; lex: 1,327; pascal: 366; makefile: 298
file content (313 lines) | stat: -rw-r--r-- 12,549 bytes parent folder | download | duplicates (7)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
/*
 * Copyright (C) 2012 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1.  Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 * 2.  Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "PointerLockController.h"

#if ENABLE(POINTER_LOCK)

#include "Chrome.h"
#include "ChromeClient.h"
#include "DocumentInlines.h"
#include "Element.h"
#include "Event.h"
#include "EventNames.h"
#include "ExceptionCode.h"
#include "JSDOMPromiseDeferred.h"
#include "Page.h"
#include "PlatformMouseEvent.h"
#include "PointerCaptureController.h"
#include "UserGestureIndicator.h"
#include "VoidCallback.h"
#include <wtf/TZoneMallocInlines.h>

namespace WebCore {

WTF_MAKE_TZONE_ALLOCATED_IMPL(PointerLockController);

class UnadjustedMovementPlatformMouseEvent : public PlatformMouseEvent {
public:
    explicit UnadjustedMovementPlatformMouseEvent(const PlatformMouseEvent& src)
        : PlatformMouseEvent(src)
    {
        m_movementDelta = src.unadjustedMovementDelta();
    }
};

PointerLockController::PointerLockController(Page& page)
    : m_page(page)
{
}

PointerLockController::~PointerLockController() = default;

void PointerLockController::requestPointerLock(Element* target, std::optional<PointerLockOptions>&& options, RefPtr<DeferredPromise> promise)
{
    if (!target || !target->isConnected() || m_documentOfRemovedElementWhileWaitingForUnlock) {
        enqueueEvent(eventNames().pointerlockerrorEvent, target);
        if (promise)
            promise->reject(ExceptionCode::WrongDocumentError, "Pointer lock target must be in an active document."_s);
        return;
    }

    if (m_documentAllowedToRelockWithoutUserGesture != &target->document() && !UserGestureIndicator::processingUserGesture()) {
        enqueueEvent(eventNames().pointerlockerrorEvent, target);
        // If the request was not started from an engagement gesture and the Document has not previously released a successful Pointer Lock with exitPointerLock():
        if (promise)
            promise->reject(ExceptionCode::NotAllowedError, "Pointer lock requires a user gesture."_s);
        return;
    }

    if (target->document().isSandboxed(SandboxFlag::PointerLock)) {
        auto reason = "Blocked pointer lock on an element because the element's frame is sandboxed and the 'allow-pointer-lock' permission is not set."_s;
        // FIXME: This message should be moved off the console once a solution to https://bugs.webkit.org/show_bug.cgi?id=103274 exists.
        target->document().addConsoleMessage(MessageSource::Security, MessageLevel::Error, reason);
        enqueueEvent(eventNames().pointerlockerrorEvent, target);
        // If this's node document's active sandboxing flag set has the sandboxed pointer lock browsing context flag set:
        if (promise)
            promise->reject(ExceptionCode::SecurityError, reason);
        return;
    }

    if (options && options->unadjustedMovement && !supportsUnadjustedMovement()) {
        // If options["unadjustedMovement"] is true and the platform does not support unadjustedMovement:
        enqueueEvent(eventNames().pointerlockerrorEvent, target);
        if (promise)
            promise->reject(ExceptionCode::NotSupportedError, "Unadjusted movement is unavailable."_s);
        return;
    }

    if (m_element) {
        if (&m_element->document() != &target->document()) {
            enqueueEvent(eventNames().pointerlockerrorEvent, target);
            // If the user agent's pointer-lock target is an element whose shadow-including root is not equal to this's shadow-including root, then:
            if (promise)
                promise->reject(ExceptionCode::InvalidStateError, "Pointer lock cannot be moved to an element in a different document."_s);
            return;
        }
        m_element = target;
        m_options = WTFMove(options);
        if (m_lockPending) {
            // m_lockPending means an answer from the ChromeClient for a previous requestPointerLock on the page is pending. It's currently unknown which way that will go, whether the request will be approved or rejected. Therefore queue the promise for later when that's known and don't send out any pointerlockchangeEvent yet.
            if (promise)
                m_promises.append(promise.releaseNonNull());
        } else {
            // !m_lockPending means the pointer lock is currently held on the page, and page is just re-targeting it or changing the options.
            enqueueEvent(eventNames().pointerlockchangeEvent, target);
            if (promise)
                promise->resolve();
            // FIXME: https://bugs.webkit.org/show_bug.cgi?id=261786
            // This probably needs to be called in all code paths.
            m_page.pointerCaptureController().pointerLockWasApplied();
        }
    } else {
        m_lockPending = true;
        m_element = target;
        m_options = WTFMove(options);
        if (promise)
            m_promises.append(promise.releaseNonNull());
        // ChromeClient::requestPointerLock() can call back into didAcquirePointerLock(), so all state including element, options, and promise needs to be stored before it is called.
        if (!m_page.chrome().client().requestPointerLock()) {
            enqueueEvent(eventNames().pointerlockerrorEvent, target);
            rejectPromises(ExceptionCode::NotSupportedError, "Pointer lock is unavailable."_s);
            clearElement();
        }
    }
}

void PointerLockController::requestPointerUnlock()
{
    if (!m_element)
        return;

    m_unlockPending = true;
    m_page.chrome().client().requestPointerUnlock();
}

void PointerLockController::requestPointerUnlockAndForceCursorVisible()
{
    m_documentAllowedToRelockWithoutUserGesture = nullptr;

    if (!m_element)
        return;

    m_unlockPending = true;
    m_page.chrome().client().requestPointerUnlock();
    m_forceCursorVisibleUponUnlock = true;
}

void PointerLockController::elementWasRemovedInternal()
{
    m_documentOfRemovedElementWhileWaitingForUnlock = m_element->document();
    requestPointerUnlock();
    // It is possible that the element is removed while pointer lock is still pending.
    // This is essentially the same situation as the !target->isConnected() test in
    // PointerLockController::requestPointerLock, but it has arisen in between then and now.
    rejectPromises(ExceptionCode::WrongDocumentError, "Pointer lock target element was removed."_s);
    // Set element null immediately to block any future interaction with it
    // including mouse events received before the unlock completes.
    clearElement();
}

void PointerLockController::documentDetached(Document& document)
{
    if (m_documentAllowedToRelockWithoutUserGesture == &document)
        m_documentAllowedToRelockWithoutUserGesture = nullptr;

    if (m_element && &m_element->document() == &document) {
        m_documentOfRemovedElementWhileWaitingForUnlock = m_element->document();
        requestPointerUnlock();
        // It is possible that the document is detached while pointer lock is still pending.
        rejectPromises(ExceptionCode::WrongDocumentError, "Pointer lock target document was detached."_s);
        clearElement();
    }
}

bool PointerLockController::isLocked() const
{
    return m_element && !m_lockPending;
}

bool PointerLockController::lockPending() const
{
    return m_lockPending;
}

Element* PointerLockController::element() const
{
    return m_element.get();
}

void PointerLockController::didAcquirePointerLock()
{
    if (!m_lockPending)
        return;

    ASSERT(m_element);

    enqueueEvent(eventNames().pointerlockchangeEvent, m_element.copyRef().get());
    resolvePromises();
    m_lockPending = false;
    m_forceCursorVisibleUponUnlock = false;
    m_documentAllowedToRelockWithoutUserGesture = m_element->document();
}

void PointerLockController::didNotAcquirePointerLock()
{
    // didNotAcquirePointerLock is sent in response to ChromeClient::requestPointerLock if the window does not have focus or the UI delegate is not allowing pointer lock.
    // From the draft spec:
    // If the this's shadow-including root is the active document of a browsing context which is not (or has an ancestor browsing context which is not) in focus by a window which is in focus by the operating system's window manager:
    // 1. Fire an event named pointerlockerror at this's node document.
    // 2. Reject promise with a "WrongDocumentError" DOMException.

    enqueueEvent(eventNames().pointerlockerrorEvent, m_element.copyRef().get());
    rejectPromises(ExceptionCode::WrongDocumentError, "Pointer lock requires the window to have focus."_s);
    clearElement();
    m_unlockPending = false;
    m_documentOfRemovedElementWhileWaitingForUnlock = nullptr;
}

void PointerLockController::didLosePointerLock()
{
    if (!m_unlockPending)
        m_documentAllowedToRelockWithoutUserGesture = nullptr;

    enqueueEvent(eventNames().pointerlockchangeEvent, m_element ? m_element->protectedDocument().ptr() : RefPtr { m_documentOfRemovedElementWhileWaitingForUnlock.get() }.get());
    clearElement();
    m_unlockPending = false;
    m_documentOfRemovedElementWhileWaitingForUnlock = nullptr;
    if (m_forceCursorVisibleUponUnlock) {
        m_forceCursorVisibleUponUnlock = false;
        m_page.chrome().client().setCursorHiddenUntilMouseMoves(false);
    }
}

void PointerLockController::dispatchLockedMouseEvent(const PlatformMouseEvent& event, const AtomString& eventType)
{
    if (!m_element || !m_element->document().frame())
        return;

    Ref protectedElement { *m_element };
    protectedElement->dispatchMouseEvent((m_options && m_options->unadjustedMovement) ? UnadjustedMovementPlatformMouseEvent(event) : event, eventType, event.clickCount());

    // Create click events
    if (eventType == eventNames().mouseupEvent)
        protectedElement->dispatchMouseEvent(event, eventNames().clickEvent, event.clickCount());
}

void PointerLockController::dispatchLockedWheelEvent(const PlatformWheelEvent& event)
{
    if (!m_element || !m_element->document().frame())
        return;

    OptionSet<EventHandling> defaultHandling;
    m_element->dispatchWheelEvent(event, defaultHandling);
}

void PointerLockController::clearElement()
{
    m_lockPending = false;
    m_element = nullptr;
    m_options = std::nullopt;
    ASSERT(m_promises.isEmpty());
}

void PointerLockController::enqueueEvent(const AtomString& type, Element* element)
{
    if (element)
        enqueueEvent(type, element->protectedDocument().ptr());
}

void PointerLockController::enqueueEvent(const AtomString& type, Document* document)
{
    // FIXME: Spec doesn't specify which task source use.
    if (document)
        document->queueTaskToDispatchEvent(TaskSource::UserInteraction, Event::create(type, Event::CanBubble::Yes, Event::IsCancelable::No));
}

void PointerLockController::resolvePromises()
{
    for (auto& promise : std::exchange(m_promises, { }))
        promise->resolve();
}

void PointerLockController::rejectPromises(ExceptionCode code, const String& reason)
{
    for (auto& promise : std::exchange(m_promises, { }))
        promise->reject(code, reason);
}

bool PointerLockController::supportsUnadjustedMovement()
{
#if HAVE(MOUSE_UNACCELERATED_MOVEMENT)
    return true;
#else
    return false;
#endif
}


} // namespace WebCore

#endif // ENABLE(POINTER_LOCK)