blob: 0aadbac06709473f28c3d588ba6eee34ed8a3915 [file] [log] [blame]
// Copyright 2017 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 "chrome/browser/vr/ui_input_manager.h"
#include <algorithm>
#include "base/containers/adapters.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "chrome/browser/vr/elements/ui_element.h"
#include "chrome/browser/vr/input_event.h"
#include "chrome/browser/vr/model/reticle_model.h"
#include "chrome/browser/vr/model/text_input_info.h"
#include "chrome/browser/vr/render_info.h"
#include "chrome/browser/vr/ui_renderer.h"
#include "chrome/browser/vr/ui_scene.h"
namespace vr {
namespace {
constexpr gfx::PointF kInvalidTargetPoint =
gfx::PointF(std::numeric_limits<float>::max(),
std::numeric_limits<float>::max());
constexpr float kControllerFocusThresholdSeconds = 1.0f;
bool IsCentroidInViewport(const gfx::Transform& view_proj_matrix,
const gfx::Transform& world_matrix) {
if (world_matrix.IsIdentity()) {
// Uninitialized matrices are considered out of the viewport.
return false;
}
gfx::Transform m = view_proj_matrix * world_matrix;
gfx::Point3F o;
m.TransformPoint(&o);
return o.x() > -1.0f && o.x() < 1.0f && o.y() > -1.0f && o.y() < 1.0f;
}
bool IsScrollOrFling(const InputEventList& list) {
if (list.empty()) {
return false;
}
// We assume that we only need to consider the first gesture in the list.
auto type = list.front()->type();
return InputEvent::IsScrollEventType(type) ||
type == InputEvent::kFlingCancel;
}
void HitTestElements(UiScene* scene,
ReticleModel* reticle_model,
HitTestRequest* request) {
std::vector<const UiElement*> elements = scene->GetElementsToHitTest();
std::vector<const UiElement*> sorted =
UiRenderer::GetElementsInDrawOrder(elements);
for (const auto* element : base::Reversed(sorted)) {
DCHECK(element->IsHitTestable());
HitTestResult result;
element->HitTest(*request, &result);
if (result.type != HitTestResult::Type::kHits) {
continue;
}
reticle_model->target_element_id = element->id();
reticle_model->target_local_point = result.local_hit_point;
reticle_model->target_point = result.hit_point;
reticle_model->cursor_type = element->cursor_type();
break;
}
}
} // namespace
UiInputManager::UiInputManager(UiScene* scene) : scene_(scene) {}
UiInputManager::~UiInputManager() {}
void UiInputManager::HandleInput(base::TimeTicks current_time,
const RenderInfo& render_info,
const ControllerModel& controller_model,
ReticleModel* reticle_model,
InputEventList* input_event_list) {
UpdateControllerFocusState(current_time, render_info, controller_model);
reticle_model->target_element_id = 0;
reticle_model->target_local_point = kInvalidTargetPoint;
UiElement* target_element =
GetTargetElement(controller_model, reticle_model, *input_event_list);
auto element_local_point = reticle_model->target_local_point;
if (input_capture_element_id_)
element_local_point =
GetCapturedElementHitPoint(reticle_model->target_point);
// Sending end and cancel events.
SendFlingCancel(input_event_list, element_local_point);
SendScrollEnd(input_event_list, element_local_point,
controller_model.touchpad_button_state);
SendButtonUp(element_local_point, controller_model.touchpad_button_state,
controller_model.last_button_timestamp);
SendHoverLeave(target_element, controller_model.last_orientation_timestamp);
// Sending update events.
if (in_scroll_) {
SendScrollUpdate(input_event_list, element_local_point);
} else if (in_click_) {
SendTouchMove(element_local_point,
controller_model.last_orientation_timestamp);
} else {
SendHoverMove(target_element, reticle_model->target_local_point,
controller_model.last_orientation_timestamp);
}
// Sending begin events.
SendHoverEnter(target_element, reticle_model->target_local_point,
controller_model.last_orientation_timestamp);
SendScrollBegin(target_element, input_event_list, element_local_point);
SendButtonDown(target_element, reticle_model->target_local_point,
controller_model.touchpad_button_state,
controller_model.last_button_timestamp);
previous_button_state_ = controller_model.touchpad_button_state;
}
void UiInputManager::OnPause() {
if (hover_target_id_) {
UiElement* prev_hovered = scene_->GetUiElementById(hover_target_id_);
if (prev_hovered)
prev_hovered->OnHoverLeave(base::TimeTicks::Now());
hover_target_id_ = 0;
}
}
void UiInputManager::SendFlingCancel(InputEventList* input_event_list,
const gfx::PointF& target_point) {
if (!fling_target_id_) {
return;
}
if (input_event_list->empty() ||
(input_event_list->front()->type() != InputEvent::kFlingCancel)) {
return;
}
// Scrolling currently only supported on content window.
UiElement* element = scene_->GetUiElementById(fling_target_id_);
if (element) {
DCHECK(element->scrollable());
element->OnFlingCancel(std::move(input_event_list->front()), target_point);
}
input_event_list->erase(input_event_list->begin());
fling_target_id_ = 0;
}
void UiInputManager::SendScrollEnd(InputEventList* input_event_list,
const gfx::PointF& target_point,
ControllerModel::ButtonState button_state) {
if (!in_scroll_) {
return;
}
DCHECK_GT(input_capture_element_id_, 0);
UiElement* element = scene_->GetUiElementById(input_capture_element_id_);
if (previous_button_state_ != button_state &&
button_state == ControllerModel::ButtonState::kDown) {
DCHECK_GT(input_event_list->size(), 0LU);
DCHECK_EQ(input_event_list->front()->type(), InputEvent::kScrollEnd);
}
DCHECK(!element || element->scrollable());
if (input_event_list->empty() ||
input_event_list->front()->type() != InputEvent::kScrollEnd) {
return;
}
DCHECK_LE(input_event_list->size(), 1LU);
fling_target_id_ = input_capture_element_id_;
element->OnScrollEnd(std::move(input_event_list->front()), target_point);
input_event_list->erase(input_event_list->begin());
input_capture_element_id_ = 0;
in_scroll_ = false;
}
void UiInputManager::SendScrollBegin(UiElement* target,
InputEventList* input_event_list,
const gfx::PointF& target_point) {
if (in_scroll_ || !target || !target->scrollable())
return;
if (input_event_list->empty() ||
input_event_list->front()->type() != InputEvent::kScrollBegin) {
return;
}
input_capture_element_id_ = target->id();
in_scroll_ = true;
target->OnScrollBegin(std::move(input_event_list->front()), target_point);
input_event_list->erase(input_event_list->begin());
}
void UiInputManager::SendScrollUpdate(InputEventList* input_event_list,
const gfx::PointF& target_point) {
DCHECK(input_capture_element_id_);
if (input_event_list->empty() ||
(input_event_list->front()->type() != InputEvent::kScrollUpdate)) {
return;
}
// Scrolling currently only supported on content window.
UiElement* element = scene_->GetUiElementById(input_capture_element_id_);
if (element) {
DCHECK(element->scrollable());
element->OnScrollUpdate(std::move(input_event_list->front()), target_point);
}
input_event_list->erase(input_event_list->begin());
}
void UiInputManager::SendHoverLeave(UiElement* current_target,
base::TimeTicks timestamp) {
if (hover_target_id_ &&
(!current_target || current_target->id() != hover_target_id_)) {
UiElement* prev_hovered = scene_->GetUiElementById(hover_target_id_);
if (prev_hovered)
prev_hovered->OnHoverLeave(timestamp);
hover_target_id_ = 0;
}
}
void UiInputManager::SendHoverEnter(UiElement* target,
const gfx::PointF& target_point,
base::TimeTicks timestamp) {
if (!target || target->id() == hover_target_id_)
return;
if ((in_click_ || in_scroll_) && target->id() != input_capture_element_id_)
return;
target->OnHoverEnter(target_point, timestamp);
hover_target_id_ = target->id();
}
void UiInputManager::SendHoverMove(UiElement* target,
const gfx::PointF& target_point,
base::TimeTicks timestamp) {
if (target && target->id() == hover_target_id_)
target->OnHoverMove(target_point, timestamp);
}
void UiInputManager::SendButtonUp(const gfx::PointF& target_point,
ControllerModel::ButtonState button_state,
base::TimeTicks timestamp) {
if (!in_click_ || previous_button_state_ == button_state ||
button_state != ControllerModel::ButtonState::kUp) {
return;
}
in_click_ = false;
if (!input_capture_element_id_)
return;
UiElement* element = scene_->GetUiElementById(input_capture_element_id_);
if (element) {
element->OnButtonUp(target_point, timestamp);
// Clicking outside of the focused element causes it to lose focus.
if (element->id() != focused_element_id_ && element->focusable())
UnfocusFocusedElement();
}
input_capture_element_id_ = 0;
}
void UiInputManager::SendButtonDown(UiElement* target,
const gfx::PointF& target_point,
ControllerModel::ButtonState button_state,
base::TimeTicks timestamp) {
if (previous_button_state_ == button_state ||
button_state != ControllerModel::ButtonState::kDown) {
return;
}
in_click_ = true;
if (target) {
target->OnButtonDown(target_point, timestamp);
input_capture_element_id_ = target->id();
} else {
input_capture_element_id_ = 0;
}
}
void UiInputManager::SendTouchMove(const gfx::PointF& target_point,
base::TimeTicks timestamp) {
if (!input_capture_element_id_)
return;
UiElement* element = scene_->GetUiElementById(input_capture_element_id_);
if (element)
element->OnTouchMove(target_point, timestamp);
}
UiElement* UiInputManager::GetTargetElement(
const ControllerModel& controller_model,
ReticleModel* reticle_model,
const InputEventList& input_event_list) const {
// If we place the reticle based on elements intersecting the controller beam,
// we can end up with the reticle hiding behind elements, or jumping laterally
// in the field of view. This is physically correct, but hard to use. For
// usability, do the following instead:
//
// - Project the controller laser onto a distance-limiting sphere.
// - Create a vector between the eyes and the point on the sphere.
// - If any UI elements intersect this vector, and are within the bounding
// sphere, choose the element that is last in scene draw order (which is
// typically the closest to the eye).
// Compute the distance from the eyes to the distance limiting sphere. Note
// that the sphere is centered at the controller, rather than the eye, for
// simplicity.
float distance = scene_->background_distance();
reticle_model->target_point =
controller_model.laser_origin +
gfx::ScaleVector3d(controller_model.laser_direction, distance);
// Determine which UI element (if any) intersects the line between the ray
// origin and the controller target position. The ray origin will typically be
// the world origin (roughly the eye) to make targeting with a real controller
// more intuitive. For testing, however, we occasionally hit test along the
// laser precisely since this geometric accuracy is important and we are not
// dealing with a physical controller.
gfx::Point3F ray_origin;
if (hit_test_strategy_ == HitTestStrategy::PROJECT_TO_LASER_ORIGIN_FOR_TEST) {
ray_origin = controller_model.laser_origin;
}
float distance_limit = (reticle_model->target_point - ray_origin).Length();
HitTestRequest request;
request.ray_origin = ray_origin;
request.ray_target = reticle_model->target_point;
request.max_distance_to_plane = distance_limit;
HitTestElements(scene_, reticle_model, &request);
// TODO(vollick): support multiple dispatch. We may want to, for example,
// dispatch raw events to several elements we hit (imagine nested horizontal
// and vertical scrollers). Currently, we only dispatch to one "winner".
UiElement* target_element =
scene_->GetUiElementById(reticle_model->target_element_id);
if (target_element) {
if (IsScrollOrFling(input_event_list) && !input_capture_element_id_) {
DCHECK(!in_scroll_ && !in_click_);
UiElement* ancestor = target_element;
while (!ancestor->scrollable() && ancestor->parent())
ancestor = ancestor->parent();
if (ancestor->scrollable())
target_element = ancestor;
}
}
return target_element;
}
void UiInputManager::UpdateControllerFocusState(
base::TimeTicks current_time,
const RenderInfo& render_info,
const ControllerModel& controller_model) {
if (!IsCentroidInViewport(render_info.left_eye_model.view_proj_matrix,
controller_model.transform) &&
!IsCentroidInViewport(render_info.right_eye_model.view_proj_matrix,
controller_model.transform)) {
last_controller_outside_viewport_time_ = current_time;
controller_resting_in_viewport_ = false;
return;
}
controller_resting_in_viewport_ =
(current_time - last_controller_outside_viewport_time_).InSecondsF() >
kControllerFocusThresholdSeconds;
}
void UiInputManager::UnfocusFocusedElement() {
if (!focused_element_id_)
return;
UiElement* focused = scene_->GetUiElementById(focused_element_id_);
if (focused && focused->focusable()) {
focused->OnFocusChanged(false);
}
focused_element_id_ = 0;
}
void UiInputManager::RequestFocus(int element_id) {
if (element_id == focused_element_id_)
return;
UnfocusFocusedElement();
UiElement* focused = scene_->GetUiElementById(element_id);
if (!focused || !focused->focusable())
return;
focused_element_id_ = element_id;
focused->OnFocusChanged(true);
}
void UiInputManager::RequestUnfocus(int element_id) {
if (element_id != focused_element_id_)
return;
UnfocusFocusedElement();
}
void UiInputManager::OnInputEdited(const EditedText& info) {
UiElement* focused = scene_->GetUiElementById(focused_element_id_);
if (!focused)
return;
DCHECK(focused->focusable());
focused->OnInputEdited(info);
}
void UiInputManager::OnInputCommitted(const EditedText& info) {
UiElement* focused = scene_->GetUiElementById(focused_element_id_);
if (!focused || !focused->focusable())
return;
DCHECK(focused->focusable());
focused->OnInputCommitted(info);
}
void UiInputManager::OnKeyboardHidden() {
UnfocusFocusedElement();
}
bool UiInputManager::ControllerRestingInViewport() const {
return controller_resting_in_viewport_;
}
gfx::PointF UiInputManager::GetCapturedElementHitPoint(
const gfx::Point3F& target_point) const {
UiElement* captured_element =
scene_->GetUiElementById(input_capture_element_id_);
if (captured_element && captured_element->IsVisible()) {
HitTestRequest request;
request.ray_target = target_point;
request.max_distance_to_plane = 2 * scene_->background_distance();
HitTestResult result;
captured_element->HitTest(request, &result);
if (result.type != HitTestResult::Type::kNone)
return result.local_hit_point;
}
return kInvalidTargetPoint;
}
} // namespace vr