blob: 83cf68c10f290143d99058e14477a3ad4a8ab692 [file] [log] [blame]
/*
* 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 "third_party/blink/renderer/core/page/pointer_lock_controller.h"
#include "services/network/public/mojom/web_sandbox_flags.mojom-blink.h"
#include "third_party/blink/public/common/input/web_mouse_event.h"
#include "third_party/blink/public/mojom/input/pointer_lock_result.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_pointer_lock_options.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/input/event_handler.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/page/chrome_client.h"
#include "third_party/blink/renderer/core/page/focus_controller.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/widget/frame_widget.h"
namespace blink {
PointerLockController::PointerLockController(Page* page)
: page_(page), lock_pending_(false) {}
bool PointerLockController::RequestPointerLock(Element* target,
ResultCallback callback) {
if (!target || !target->isConnected() ||
document_of_removed_element_while_waiting_for_unlock_ || element_) {
return false;
}
LocalDOMWindow* window = To<LocalDOMWindow>(target->GetExecutionContext());
window->GetFrame()->GetWidgetForLocalRoot()->RequestMouseLock(
LocalFrame::HasTransientUserActivation(window->GetFrame()),
/*unadjusted_movement_requested=*/false,
WTF::Bind(&PointerLockController::LockRequestCallback,
WrapWeakPersistent(this), std::move(callback),
/*unadjusted_movement_requested=*/false));
lock_pending_ = true;
element_ = target;
return true;
}
ScriptPromise PointerLockController::RequestPointerLock(
ScriptPromiseResolver* resolver,
Element* target,
ExceptionState& exception_state,
const PointerLockOptions* options) {
ScriptPromise promise = resolver->Promise();
if (!target || !target->isConnected() ||
document_of_removed_element_while_waiting_for_unlock_) {
EnqueueEvent(event_type_names::kPointerlockerror, target);
exception_state.ThrowDOMException(DOMExceptionCode::kWrongDocumentError,
"Target Element removed from DOM");
return promise;
}
LocalDOMWindow* window = To<LocalDOMWindow>(target->GetExecutionContext());
window->CountUseOnlyInCrossOriginIframe(
WebFeature::kElementRequestPointerLockIframe);
if (target->IsInShadowTree()) {
UseCounter::Count(window, WebFeature::kElementRequestPointerLockInShadow);
}
if (options && options->unadjustedMovement()) {
UseCounter::Count(window, WebFeature::kPointerLockUnadjustedMovement);
}
if (window->IsSandboxed(
network::mojom::blink::WebSandboxFlags::kPointerLock)) {
// FIXME: This message should be moved off the console once a solution to
// https://bugs.webkit.org/show_bug.cgi?id=103274 exists.
window->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::blink::ConsoleMessageSource::kSecurity,
mojom::blink::ConsoleMessageLevel::kError,
"Blocked pointer lock on an element because the element's frame is "
"sandboxed and the 'allow-pointer-lock' permission is not set."));
EnqueueEvent(event_type_names::kPointerlockerror, target);
exception_state.ThrowSecurityError(
"Blocked pointer lock on an element because the element's frame is "
"sandboxed and the 'allow-pointer-lock' permission is not set.",
"");
return promise;
}
bool unadjusted_movement_requested =
options ? options->unadjustedMovement() : false;
if (element_) {
if (element_->GetDocument() != target->GetDocument()) {
EnqueueEvent(event_type_names::kPointerlockerror, target);
exception_state.ThrowDOMException(
DOMExceptionCode::kWrongDocumentError,
"The new element is not in the same shadow-root document as the "
"element that currently holds the lock.");
return promise;
}
// Attempt to change options if necessary.
if (unadjusted_movement_requested != current_unadjusted_movement_setting_) {
if (!mouse_lock_context_.is_bound() || lock_pending_) {
EnqueueEvent(event_type_names::kPointerlockerror, target);
exception_state.ThrowDOMException(
DOMExceptionCode::kInUseAttributeError, "Pointer lock pending.");
return promise;
}
mouse_lock_context_->RequestMouseLockChange(
unadjusted_movement_requested,
WTF::Bind(
&PointerLockController::ChangeLockRequestCallback,
WrapWeakPersistent(this), WrapWeakPersistent(target),
WTF::Bind(&PointerLockController::ProcessResultScriptPromise,
WrapPersistent(resolver)),
unadjusted_movement_requested));
return promise;
}
EnqueueEvent(event_type_names::kPointerlockchange, target);
element_ = target;
resolver->Resolve();
// Subsequent steps are handled in the browser process.
} else {
window->GetFrame()->GetWidgetForLocalRoot()->RequestMouseLock(
LocalFrame::HasTransientUserActivation(window->GetFrame()),
unadjusted_movement_requested,
WTF::Bind(&PointerLockController::LockRequestCallback,
WrapWeakPersistent(this),
WTF::Bind(&PointerLockController::ProcessResultScriptPromise,
WrapPersistent(resolver)),
unadjusted_movement_requested));
lock_pending_ = true;
element_ = target;
}
return promise;
}
void PointerLockController::ChangeLockRequestCallback(
Element* target,
ResultCallback callback,
bool unadjusted_movement_requested,
mojom::blink::PointerLockResult result) {
if (result == mojom::blink::PointerLockResult::kSuccess)
element_ = target;
ProcessResult(std::move(callback), unadjusted_movement_requested, result);
}
void PointerLockController::LockRequestCallback(
ResultCallback callback,
bool unadjusted_movement_requested,
mojom::blink::PointerLockResult result,
mojo::PendingRemote<blink::mojom::blink::PointerLockContext> context) {
if (element_ && context) {
mouse_lock_context_.Bind(std::move(context),
element_->GetExecutionContext()->GetTaskRunner(
TaskType::kUserInteraction));
// The browser might unlock the mouse for many reasons including closing
// the tab, the user hitting esc, the page losing focus, and more.
mouse_lock_context_.set_disconnect_handler(WTF::Bind(
&PointerLockController::ExitPointerLock, WrapWeakPersistent(this)));
}
ProcessResult(std::move(callback), unadjusted_movement_requested, result);
if (result == mojom::blink::PointerLockResult::kSuccess) {
DidAcquirePointerLock();
} else {
DidNotAcquirePointerLock();
}
}
void PointerLockController::ProcessResultScriptPromise(
ScriptPromiseResolver* resolver,
mojom::blink::PointerLockResult result) {
if (result == mojom::blink::PointerLockResult::kSuccess) {
resolver->Resolve();
return;
}
DOMException* exception = ConvertResultToException(result);
RejectIfPromiseEnabled(resolver, exception);
}
void PointerLockController::ProcessResult(
ResultCallback callback,
bool unadjusted_movement_requested,
mojom::blink::PointerLockResult result) {
if (result == mojom::blink::PointerLockResult::kSuccess)
current_unadjusted_movement_setting_ = unadjusted_movement_requested;
std::move(callback).Run(result);
}
DOMException* PointerLockController::ConvertResultToException(
mojom::blink::PointerLockResult result) {
switch (result) {
case mojom::blink::PointerLockResult::kUnsupportedOptions:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotSupportedError,
"The options asked for in this request are not supported on this "
"platform.");
case mojom::blink::PointerLockResult::kRequiresUserGesture:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotAllowedError,
"A user gesture is required to request Pointer Lock.");
case mojom::blink::PointerLockResult::kAlreadyLocked:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInUseAttributeError, "Pointer is already locked.");
case mojom::blink::PointerLockResult::kWrongDocument:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kWrongDocumentError,
"The root document of this element is not valid for pointer lock.");
case mojom::blink::PointerLockResult::kPermissionDenied:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"The root document of this element is not valid for pointer lock.");
case mojom::blink::PointerLockResult::kElementDestroyed:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kWrongDocumentError,
"The element has been destroyed while making this request.");
case mojom::blink::PointerLockResult::kUserRejected:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError,
"The user has exited the lock before this request was completed.");
case mojom::blink::PointerLockResult::kSuccess:
case mojom::blink::PointerLockResult::kUnknownError:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kUnknownError,
"If you see this error we have a bug. Please report this bug to "
"chromium.");
}
}
void PointerLockController::RejectIfPromiseEnabled(
ScriptPromiseResolver* resolver,
DOMException* exception) {
if (RuntimeEnabledFeatures::PointerLockOptionsEnabled(
resolver->GetExecutionContext())) {
resolver->Reject(exception);
}
}
void PointerLockController::ExitPointerLock() {
Document* pointer_lock_document =
element_ ? &element_->GetDocument()
: document_of_removed_element_while_waiting_for_unlock_.Get();
EnqueueEvent(event_type_names::kPointerlockchange, pointer_lock_document);
// Set the last mouse position back the locked position.
if (pointer_lock_document && pointer_lock_document->GetFrame()) {
pointer_lock_document->GetFrame()
->GetEventHandler()
.ResetMousePositionForPointerUnlock();
}
ClearElement();
document_of_removed_element_while_waiting_for_unlock_ = nullptr;
mouse_lock_context_.reset();
}
void PointerLockController::ElementRemoved(Element* element) {
if (element_ == element) {
document_of_removed_element_while_waiting_for_unlock_ =
&element_->GetDocument();
ExitPointerLock();
// 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 (element_ && element_->GetDocument() == document) {
ExitPointerLock();
ClearElement();
}
}
bool PointerLockController::LockPending() const {
return lock_pending_;
}
bool PointerLockController::IsPointerLocked() const {
return mouse_lock_context_.is_bound();
}
Element* PointerLockController::GetElement() const {
return element_.Get();
}
void PointerLockController::DidAcquirePointerLock() {
EnqueueEvent(event_type_names::kPointerlockchange, element_.Get());
lock_pending_ = false;
if (element_) {
LocalFrame* frame = element_->GetDocument().GetFrame();
pointer_lock_position_ = frame->LocalFrameRoot()
.GetEventHandler()
.LastKnownMousePositionInRootFrame();
pointer_lock_screen_position_ = frame->LocalFrameRoot()
.GetEventHandler()
.LastKnownMouseScreenPosition();
LocalFrame* focused_frame =
frame->GetPage()->GetFocusController().FocusedFrame();
if (focused_frame) {
focused_frame->GetEventHandler().ReleaseMousePointerCapture();
}
// Mouse Lock removes the system cursor and provides all mouse motion as
// .movementX/Y values on events all sent to a fixed target. This requires
// content to specifically request the mode to be entered.
// Mouse Capture is implicitly given for the duration of a drag event, and
// sends all mouse events to the initial target of the drag.
// If Lock is entered it supersedes any in progress Capture.
frame->GetWidgetForLocalRoot()->MouseCaptureLost();
}
}
void PointerLockController::DidNotAcquirePointerLock() {
EnqueueEvent(event_type_names::kPointerlockerror, element_.Get());
ClearElement();
}
void PointerLockController::DispatchLockedMouseEvent(
const WebMouseEvent& event,
const Vector<WebMouseEvent>& coalesced_events,
const Vector<WebMouseEvent>& predicted_events,
const AtomicString& event_type) {
if (!element_ || !element_->GetDocument().GetFrame())
return;
if (LocalFrame* frame = element_->GetDocument().GetFrame()) {
frame->GetEventHandler().HandleTargetedMouseEvent(
element_, event, event_type, coalesced_events, predicted_events);
// Event handlers may remove element.
if (!element_)
return;
// Create click events
if (event_type == event_type_names::kMouseup) {
frame->GetEventHandler().HandleTargetedMouseEvent(
element_, event, event_type_names::kClick, Vector<WebMouseEvent>(),
Vector<WebMouseEvent>());
}
}
}
void PointerLockController::GetPointerLockPosition(
FloatPoint* lock_position,
FloatPoint* lock_screen_position) {
if (element_ && !lock_pending_) {
DCHECK(lock_position);
DCHECK(lock_screen_position);
*lock_position = pointer_lock_position_;
*lock_screen_position = pointer_lock_screen_position_;
}
}
void PointerLockController::ClearElement() {
lock_pending_ = false;
element_ = nullptr;
}
void PointerLockController::EnqueueEvent(const AtomicString& type,
Element* element) {
if (element)
EnqueueEvent(type, &element->GetDocument());
}
void PointerLockController::EnqueueEvent(const AtomicString& type,
Document* document) {
if (document && document->domWindow()) {
document->domWindow()->EnqueueDocumentEvent(*Event::Create(type),
TaskType::kUserInteraction);
}
}
void PointerLockController::Trace(Visitor* visitor) const {
visitor->Trace(page_);
visitor->Trace(element_);
visitor->Trace(document_of_removed_element_while_waiting_for_unlock_);
visitor->Trace(mouse_lock_context_);
}
// static
Element* PointerLockController::GetPointerLockedElement(LocalFrame* frame) {
if (Page* p = frame->GetPage()) {
if (!p->GetPointerLockController().LockPending())
return p->GetPointerLockController().GetElement();
}
return nullptr;
}
} // namespace blink