blob: cd7d27d755683edd927a6aae82537f78ab1ca3d4 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/xr/xr_input_source.h"
#include "base/time/time.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/events/event_dispatcher.h"
#include "third_party/blink/renderer/core/dom/events/event_path.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/html_frame_element_base.h"
#include "third_party/blink/renderer/core/input/event_handling_util.h"
#include "third_party/blink/renderer/core/layout/hit_test_location.h"
#include "third_party/blink/renderer/modules/xr/xr_grip_space.h"
#include "third_party/blink/renderer/modules/xr/xr_hand.h"
#include "third_party/blink/renderer/modules/xr/xr_input_source_event.h"
#include "third_party/blink/renderer/modules/xr/xr_session.h"
#include "third_party/blink/renderer/modules/xr/xr_session_event.h"
#include "third_party/blink/renderer/modules/xr/xr_space.h"
#include "third_party/blink/renderer/modules/xr/xr_system.h"
#include "third_party/blink/renderer/modules/xr/xr_target_ray_space.h"
#include "third_party/blink/renderer/modules/xr/xr_utils.h"
namespace blink {
namespace {
std::unique_ptr<TransformationMatrix> TryGetTransformationMatrix(
const base::Optional<gfx::Transform>& transform) {
if (transform) {
return std::make_unique<TransformationMatrix>(transform->matrix());
}
return nullptr;
}
std::unique_ptr<TransformationMatrix> TryGetTransformationMatrix(
const TransformationMatrix* other) {
if (other) {
return std::make_unique<TransformationMatrix>(*other);
}
return nullptr;
}
} // namespace
XRInputSource::InternalState::InternalState(
uint32_t source_id,
device::mojom::XRTargetRayMode target_ray_mode,
base::TimeTicks base_timestamp)
: source_id(source_id),
target_ray_mode(target_ray_mode),
base_timestamp(base_timestamp) {}
XRInputSource::InternalState::InternalState(const InternalState& other) =
default;
XRInputSource::InternalState::~InternalState() = default;
XRInputSource* XRInputSource::CreateOrUpdateFrom(
XRInputSource* other,
XRSession* session,
const device::mojom::blink::XRInputSourceStatePtr& state) {
if (!state)
return other;
XRInputSource* updated_source = other;
// Check if we have an existing object, and if we do, if it can be re-used.
if (!other) {
auto source_id = state->source_id;
updated_source = MakeGarbageCollected<XRInputSource>(session, source_id);
} else if (other->InvalidatesSameObject(state)) {
// Something in the state has changed which requires us to re-create the
// object. Create a copy now, and we will blindly update any state later,
// knowing that we now have a new object if needed.
updated_source = MakeGarbageCollected<XRInputSource>(*other);
}
if (updated_source->state_.is_visible) {
updated_source->UpdateGamepad(state->gamepad);
}
// Update the input source's description if this state update includes them.
if (state->description) {
const device::mojom::blink::XRInputSourceDescriptionPtr& desc =
state->description;
updated_source->state_.target_ray_mode = desc->target_ray_mode;
updated_source->state_.handedness = desc->handedness;
if (updated_source->state_.is_visible) {
updated_source->input_from_pointer_ =
TryGetTransformationMatrix(desc->input_from_pointer);
}
updated_source->state_.profiles.clear();
for (const auto& name : state->description->profiles) {
updated_source->state_.profiles.push_back(name);
}
}
if (updated_source->state_.is_visible) {
updated_source->mojo_from_input_ =
TryGetTransformationMatrix(state->mojo_from_input);
}
if (updated_source->state_.is_visible) {
if (state->hand_tracking_data.get()) {
updated_source->hand_ = MakeGarbageCollected<XRHand>(
state->hand_tracking_data.get(), updated_source);
}
}
updated_source->state_.emulated_position = state->emulated_position;
return updated_source;
}
XRInputSource::XRInputSource(XRSession* session,
uint32_t source_id,
device::mojom::XRTargetRayMode target_ray_mode)
: state_(source_id, target_ray_mode, session->xr()->NavigationStart()),
session_(session),
target_ray_space_(MakeGarbageCollected<XRTargetRaySpace>(session, this)),
grip_space_(MakeGarbageCollected<XRGripSpace>(session, this)) {}
// Must make new target_ray_space_ and grip_space_ to ensure that they point to
// the correct XRInputSource object. Otherwise, the controller position gets
// stuck when an XRInputSource gets re-created. Also need to make a deep copy of
// the matrices since they use unique_ptrs.
XRInputSource::XRInputSource(const XRInputSource& other)
: state_(other.state_),
session_(other.session_),
target_ray_space_(
MakeGarbageCollected<XRTargetRaySpace>(other.session_, this)),
grip_space_(MakeGarbageCollected<XRGripSpace>(other.session_, this)),
gamepad_(other.gamepad_),
hand_(other.hand_),
mojo_from_input_(
TryGetTransformationMatrix(other.mojo_from_input_.get())),
input_from_pointer_(
TryGetTransformationMatrix(other.input_from_pointer_.get())) {}
const String XRInputSource::handedness() const {
switch (state_.handedness) {
case device::mojom::XRHandedness::NONE:
return "none";
case device::mojom::XRHandedness::LEFT:
return "left";
case device::mojom::XRHandedness::RIGHT:
return "right";
}
NOTREACHED() << "Unknown handedness: " << state_.handedness;
}
const String XRInputSource::targetRayMode() const {
switch (state_.target_ray_mode) {
case device::mojom::XRTargetRayMode::GAZING:
return "gaze";
case device::mojom::XRTargetRayMode::POINTING:
return "tracked-pointer";
case device::mojom::XRTargetRayMode::TAPPING:
return "screen";
}
NOTREACHED() << "Unknown target ray mode: " << state_.target_ray_mode;
}
XRSpace* XRInputSource::targetRaySpace() const {
return target_ray_space_;
}
XRSpace* XRInputSource::gripSpace() const {
if (!state_.is_visible)
return nullptr;
if (state_.target_ray_mode == device::mojom::XRTargetRayMode::POINTING) {
return grip_space_;
}
return nullptr;
}
bool XRInputSource::InvalidatesSameObject(
const device::mojom::blink::XRInputSourceStatePtr& state) {
if ((state->gamepad && !gamepad_) || (!state->gamepad && gamepad_)) {
return true;
}
if (state->description) {
if (state->description->handedness != state_.handedness) {
return true;
}
if (state->description->target_ray_mode != state_.target_ray_mode) {
return true;
}
if (state->description->profiles.size() != state_.profiles.size()) {
return true;
}
for (wtf_size_t i = 0; i < state_.profiles.size(); ++i) {
if (state->description->profiles[i] != state_.profiles[i]) {
return true;
}
}
}
return false;
}
void XRInputSource::SetInputFromPointer(
const TransformationMatrix* input_from_pointer) {
if (state_.is_visible) {
input_from_pointer_ = TryGetTransformationMatrix(input_from_pointer);
}
}
void XRInputSource::SetGamepadConnected(bool state) {
if (gamepad_)
gamepad_->SetConnected(state);
}
void XRInputSource::UpdateGamepad(
const base::Optional<device::Gamepad>& gamepad) {
if (gamepad) {
if (!gamepad_) {
gamepad_ = MakeGarbageCollected<Gamepad>(this, -1, state_.base_timestamp,
base::TimeTicks::Now());
}
gamepad_->UpdateFromDeviceState(*gamepad);
} else {
gamepad_ = nullptr;
}
}
base::Optional<TransformationMatrix> XRInputSource::MojoFromInput() const {
if (!mojo_from_input_.get()) {
return base::nullopt;
}
return *(mojo_from_input_.get());
}
base::Optional<TransformationMatrix> XRInputSource::InputFromPointer() const {
if (!input_from_pointer_.get()) {
return base::nullopt;
}
return *(input_from_pointer_.get());
}
base::Optional<device::mojom::blink::XRNativeOriginInformation>
XRInputSource::nativeOrigin() const {
return XRNativeOriginInformation::Create(this);
}
void XRInputSource::OnSelectStart() {
DVLOG(3) << __func__;
// Discard duplicate events and ones after the session has ended.
if (state_.primary_input_pressed || session_->ended())
return;
state_.primary_input_pressed = true;
state_.selection_cancelled = false;
DVLOG(3) << __func__ << ": dispatch selectstart event";
XRInputSourceEvent* event =
CreateInputSourceEvent(event_type_names::kSelectstart);
session_->DispatchEvent(*event);
if (event->defaultPrevented())
state_.selection_cancelled = true;
// Ensure the frame cannot be used outside of the event handler.
event->frame()->Deactivate();
}
void XRInputSource::OnSelectEnd() {
DVLOG(3) << __func__;
// Discard duplicate events and ones after the session has ended.
if (!state_.primary_input_pressed || session_->ended())
return;
state_.primary_input_pressed = false;
if (!session_->xr()->DomWindow())
return;
DVLOG(3) << __func__ << ": dispatch selectend event";
XRInputSourceEvent* event =
CreateInputSourceEvent(event_type_names::kSelectend);
session_->DispatchEvent(*event);
if (event->defaultPrevented())
state_.selection_cancelled = true;
// Ensure the frame cannot be used outside of the event handler.
event->frame()->Deactivate();
}
void XRInputSource::OnSelect() {
DVLOG(3) << __func__;
// If a select was fired but we had not previously started the selection it
// indicates a sub-frame or instantaneous select event, and we should fire a
// selectstart prior to the selectend.
if (!state_.primary_input_pressed) {
OnSelectStart();
}
// If SelectStart caused the session to end, we shouldn't try to fire the
// select event.
LocalDOMWindow* window = session_->xr()->DomWindow();
if (!window)
return;
LocalFrame::NotifyUserActivation(
window->GetFrame(),
mojom::blink::UserActivationNotificationType::kInteraction);
if (!state_.selection_cancelled && !session_->ended()) {
DVLOG(3) << __func__ << ": dispatch select event";
XRInputSourceEvent* event =
CreateInputSourceEvent(event_type_names::kSelect);
session_->DispatchEvent(*event);
// Ensure the frame cannot be used outside of the event handler.
event->frame()->Deactivate();
}
OnSelectEnd();
}
void XRInputSource::OnSqueezeStart() {
DVLOG(3) << __func__;
// Discard duplicate events and ones after the session has ended.
if (state_.primary_squeeze_pressed || session_->ended())
return;
state_.primary_squeeze_pressed = true;
state_.squeezing_cancelled = false;
XRInputSourceEvent* event =
CreateInputSourceEvent(event_type_names::kSqueezestart);
session_->DispatchEvent(*event);
if (event->defaultPrevented())
state_.squeezing_cancelled = true;
// Ensure the frame cannot be used outside of the event handler.
event->frame()->Deactivate();
}
void XRInputSource::OnSqueezeEnd() {
DVLOG(3) << __func__;
// Discard duplicate events and ones after the session has ended.
if (!state_.primary_squeeze_pressed || session_->ended())
return;
state_.primary_squeeze_pressed = false;
if (!session_->xr()->DomWindow())
return;
DVLOG(3) << __func__ << ": dispatch squeezeend event";
XRInputSourceEvent* event =
CreateInputSourceEvent(event_type_names::kSqueezeend);
session_->DispatchEvent(*event);
if (event->defaultPrevented())
state_.squeezing_cancelled = true;
// Ensure the frame cannot be used outside of the event handler.
event->frame()->Deactivate();
}
void XRInputSource::OnSqueeze() {
DVLOG(3) << __func__;
// If a squeeze was fired but we had not previously started the squeezing it
// indicates a sub-frame or instantaneous squeeze event, and we should fire a
// squeezestart prior to the squeezeend.
if (!state_.primary_squeeze_pressed) {
OnSqueezeStart();
}
// If SelectStart caused the session to end, we shouldn't try to fire the
// select event.
LocalDOMWindow* window = session_->xr()->DomWindow();
if (!window)
return;
LocalFrame::NotifyUserActivation(
window->GetFrame(),
mojom::blink::UserActivationNotificationType::kInteraction);
// If SelectStart caused the session to end, we shouldn't try to fire the
// select event.
if (!state_.squeezing_cancelled && !session_->ended()) {
DVLOG(3) << __func__ << ": dispatch squeeze event";
XRInputSourceEvent* event =
CreateInputSourceEvent(event_type_names::kSqueeze);
session_->DispatchEvent(*event);
// Ensure the frame cannot be used outside of the event handler.
event->frame()->Deactivate();
}
OnSqueezeEnd();
}
void XRInputSource::UpdateButtonStates(
const device::mojom::blink::XRInputSourceStatePtr& new_state) {
if (!new_state)
return;
DVLOG(3) << __func__ << ": state_.is_visible=" << state_.is_visible
<< ", state_.xr_select_events_suppressed="
<< state_.xr_select_events_suppressed
<< ", new_state->primary_input_clicked="
<< new_state->primary_input_clicked;
if (!state_.is_visible) {
DVLOG(3) << __func__ << ": input NOT VISIBLE";
if (new_state->primary_input_clicked) {
DVLOG(3) << __func__ << ": got click while invisible, SUPPRESS end";
state_.xr_select_events_suppressed = false;
}
return;
}
if (state_.xr_select_events_suppressed) {
if (new_state->primary_input_clicked) {
DVLOG(3) << __func__ << ": got click, SUPPRESS end";
state_.xr_select_events_suppressed = false;
}
DVLOG(3) << __func__ << ": overlay input select SUPPRESSED";
return;
}
DCHECK(!state_.xr_select_events_suppressed);
// Handle state change of the primary input, which may fire events
if (new_state->primary_input_clicked)
OnSelect();
if (new_state->primary_input_pressed) {
OnSelectStart();
} else if (state_.primary_input_pressed) {
// May get here if the input source was previously pressed but now isn't,
// but the input source did not set primary_input_clicked to true. We will
// treat this as a cancelled selection, firing the selectend event so the
// page stays in sync with the controller state but won't fire the
// usual select event.
OnSelectEnd();
}
// Handle state change of the primary input, which may fire events
if (new_state->primary_squeeze_clicked)
OnSqueeze();
if (new_state->primary_squeeze_pressed) {
OnSqueezeStart();
} else if (state_.primary_squeeze_pressed) {
// May get here if the input source was previously pressed but now isn't,
// but the input source did not set primary_squeeze_clicked to true. We will
// treat this as a cancelled squeezeing, firing the squeezeend event so the
// page stays in sync with the controller state but won't fire the
// usual squeeze event.
OnSqueezeEnd();
}
}
void XRInputSource::ProcessOverlayHitTest(
Element* overlay_element,
const device::mojom::blink::XRInputSourceStatePtr& new_state) {
DVLOG(3) << __func__ << ": state_.xr_select_events_suppressed="
<< state_.xr_select_events_suppressed;
DCHECK(overlay_element);
DCHECK(new_state->overlay_pointer_position);
// Do a hit test at the overlay pointer position to see if the pointer
// intersects a cross origin iframe. If yes, set the visibility to false which
// causes targetRaySpace and gripSpace to return null poses.
FloatPoint point(new_state->overlay_pointer_position->x(),
new_state->overlay_pointer_position->y());
DVLOG(3) << __func__ << ": hit test point=" << point;
HitTestRequest::HitTestRequestType hit_type = HitTestRequest::kTouchEvent |
HitTestRequest::kReadOnly |
HitTestRequest::kActive;
HitTestResult result = event_handling_util::HitTestResultInFrame(
overlay_element->GetDocument().GetFrame(), HitTestLocation(point),
hit_type);
DVLOG(3) << __func__ << ": hit test InnerElement=" << result.InnerElement();
Element* hit_element = result.InnerElement();
if (!hit_element) {
return;
}
// Check if the hit element is cross-origin content. In addition to an iframe,
// this could potentially be an old-style frame in a frameset, so check for
// the common base class to cover both. (There's no intention to actively
// support framesets for DOM Overlay, but this helps prevent them from
// being used as a mechanism for information leaks.)
HTMLFrameElementBase* frame = DynamicTo<HTMLFrameElementBase>(hit_element);
if (frame) {
Document* hit_document = frame->contentDocument();
if (hit_document) {
Frame* hit_frame = hit_document->GetFrame();
DCHECK(hit_frame);
if (hit_frame->IsCrossOriginToMainFrame()) {
// Mark the input source as invisible until the primary button is
// released.
state_.is_visible = false;
// If this is the first touch, also suppress events, even if it
// ends up being released outside the frame later.
if (!state_.primary_input_pressed) {
state_.xr_select_events_suppressed = true;
}
DVLOG(3)
<< __func__
<< ": input source overlaps with cross origin content, is_visible="
<< state_.is_visible << ", xr_select_events_suppressed="
<< state_.xr_select_events_suppressed;
return;
}
}
}
// If we get here, the touch didn't hit a cross origin frame. Set the
// controller spaces visible.
state_.is_visible = true;
// Now that the visibility check has finished, mark non-primary input sources
// as suppressed.
if (new_state->is_auxiliary) {
state_.xr_select_events_suppressed = true;
}
// Now check if this is a new primary button press. If yes, send a
// beforexrselect event to give the application an opportunity to cancel the
// XR input "select" sequence that would normally be caused by this.
if (state_.xr_select_events_suppressed) {
DVLOG(3) << __func__ << ": using overlay input provider: SUPPRESS ongoing";
return;
}
if (state_.primary_input_pressed) {
DVLOG(3) << __func__ << ": ongoing press, not checking again";
return;
}
bool is_primary_press =
new_state->primary_input_pressed || new_state->primary_input_clicked;
if (!is_primary_press) {
DVLOG(3) << __func__ << ": no button press, ignoring";
return;
}
// The event needs to be cancelable (obviously), bubble (so that parent
// elements can handle it), and composed (so that it crosses shadow DOM
// boundaries, including UA-added shadow DOM).
Event* event = MakeGarbageCollected<XRSessionEvent>(
event_type_names::kBeforexrselect, session_, Event::Bubbles::kYes,
Event::Cancelable::kYes, Event::ComposedMode::kComposed);
hit_element->DispatchEvent(*event);
bool default_prevented = event->defaultPrevented();
// Keep the input source visible, so it's exposed in the input sources array,
// but don't generate XR select events for the current button sequence.
state_.xr_select_events_suppressed = default_prevented;
DVLOG(3) << __func__ << ": state_.xr_select_events_suppressed="
<< state_.xr_select_events_suppressed;
}
void XRInputSource::OnRemoved() {
if (state_.primary_input_pressed) {
state_.primary_input_pressed = false;
XRInputSourceEvent* event =
CreateInputSourceEvent(event_type_names::kSelectend);
session_->DispatchEvent(*event);
if (event->defaultPrevented())
state_.selection_cancelled = true;
// Ensure the frame cannot be used outside of the event handler.
event->frame()->Deactivate();
}
if (state_.primary_squeeze_pressed) {
state_.primary_squeeze_pressed = false;
XRInputSourceEvent* event =
CreateInputSourceEvent(event_type_names::kSqueezeend);
session_->DispatchEvent(*event);
if (event->defaultPrevented())
state_.squeezing_cancelled = true;
// Ensure the frame cannot be used outside of the event handler.
event->frame()->Deactivate();
}
SetGamepadConnected(false);
}
XRInputSourceEvent* XRInputSource::CreateInputSourceEvent(
const AtomicString& type) {
XRFrame* presentation_frame = session_->CreatePresentationFrame();
return XRInputSourceEvent::Create(type, presentation_frame, this);
}
void XRInputSource::Trace(Visitor* visitor) const {
visitor->Trace(session_);
visitor->Trace(target_ray_space_);
visitor->Trace(grip_space_);
visitor->Trace(gamepad_);
visitor->Trace(hand_);
ScriptWrappable::Trace(visitor);
}
} // namespace blink