| // 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 |