blob: b4e1c0a814508f317091b4999c34ebba7dba116e [file] [log] [blame]
// Copyright 2018 The Fuchsia 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 "src/ui/scenic/lib/input/input_command_dispatcher.h"
#include <fuchsia/ui/scenic/cpp/fidl.h>
#include <lib/fidl/cpp/clone.h>
#include <lib/fostr/fidl/fuchsia/ui/input/formatting.h>
#include <memory>
#include <vector>
#include <trace/event.h>
#include "src/lib/fxl/logging.h"
#include "src/ui/lib/escher/geometry/types.h"
#include "src/ui/scenic/lib/gfx/engine/hit_accumulator.h"
#include "src/ui/scenic/lib/gfx/resources/compositor/layer_stack.h"
#include "src/ui/scenic/lib/input/helper.h"
#include "src/ui/scenic/lib/input/input_system.h"
#include "src/ui/scenic/lib/scenic/event_reporter.h"
namespace scenic_impl {
namespace input {
using AccessibilityPointerEvent = fuchsia::ui::input::accessibility::PointerEvent;
using FocusChangeStatus = scenic_impl::gfx::ViewTree::FocusChangeStatus;
using InputCommand = fuchsia::ui::input::Command;
using Phase = fuchsia::ui::input::PointerEventPhase;
using ScenicCommand = fuchsia::ui::scenic::Command;
using fuchsia::ui::input::InputEvent;
using fuchsia::ui::input::PointerEvent;
using fuchsia::ui::input::PointerEventType;
using fuchsia::ui::input::SendPointerInputCmd;
namespace {
// TODO(SCN-1278): Remove this.
// Turn two floats (high bits, low bits) into a 64-bit uint.
trace_flow_id_t PointerTraceHACK(float fa, float fb) {
uint32_t ia, ib;
memcpy(&ia, &fa, sizeof(uint32_t));
memcpy(&ib, &fb, sizeof(uint32_t));
return (((uint64_t)ia) << 32) | ib;
}
gfx::LayerStackPtr GetLayerStack(gfx::Engine* engine, GlobalId compositor_id) {
FXL_DCHECK(engine);
gfx::CompositorWeakPtr compositor = engine->scene_graph()->GetCompositor(compositor_id);
FXL_DCHECK(compositor) << "No compositor, violated invariant.";
gfx::LayerStackPtr layer_stack = compositor->layer_stack();
FXL_DCHECK(layer_stack) << "No layer stack, violated invariant.";
return layer_stack;
}
// The x and y values are in device (screen) coordinates.
// The initial dispatch logic guarantees a valid compositor and layer stack.
// NOTE: The accumulated hit structs contain resources that callers should let go of as soon as
// possible.
//
// Only the root presenter creates compositors and sends input commands.
// This invariant means this dispatcher context's session, handling an input
// command, also originally created the compositor.
//
void PerformGlobalHitTest(const gfx::LayerStackPtr& layer_stack, const escher::vec2& pointer,
gfx::HitAccumulator<gfx::ViewHit>* accumulator) {
escher::ray4 ray = CreateScreenPerpendicularRay(pointer.x, pointer.y);
FXL_VLOG(1) << "HitTest: device point (" << ray.origin.x << ", " << ray.origin.y << ")";
layer_stack->HitTest(ray, accumulator);
}
// Helper function to build an AccessibilityPointerEvent when there is a
// registered accessibility listener.
AccessibilityPointerEvent BuildAccessibilityPointerEvent(const PointerEvent& original,
const escher::vec2& ndc_point,
const escher::vec2& local_point,
uint64_t viewref_koid) {
AccessibilityPointerEvent event;
event.set_event_time(original.event_time);
event.set_device_id(original.device_id);
event.set_pointer_id(original.pointer_id);
event.set_type(original.type);
event.set_phase(original.phase);
event.set_ndc_point({ndc_point.x, ndc_point.y});
event.set_viewref_koid(viewref_koid);
if (viewref_koid != ZX_KOID_INVALID) {
event.set_local_point({local_point.x, local_point.y});
}
return event;
}
} // namespace
InputCommandDispatcher::InputCommandDispatcher(scheduling::SessionId session_id,
std::shared_ptr<EventReporter> event_reporter,
gfx::Engine* engine, InputSystem* input_system)
: session_id_(session_id),
event_reporter_(std::move(event_reporter)),
engine_(engine),
input_system_(input_system) {
FXL_CHECK(engine_);
FXL_CHECK(input_system_);
}
void InputCommandDispatcher::DispatchCommand(ScenicCommand command,
scheduling::PresentId present_id) {
TRACE_DURATION("input", "dispatch_command", "command", "ScenicCmd");
FXL_DCHECK(command.Which() == ScenicCommand::Tag::kInput);
InputCommand& input = command.input();
if (input.is_send_keyboard_input()) {
DispatchCommand(input.send_keyboard_input());
} else if (input.is_send_pointer_input()) {
// Compositor and layer stack required for dispatch.
GlobalId compositor_id(session_id_, input.send_pointer_input().compositor_id);
gfx::CompositorWeakPtr compositor = engine_->scene_graph()->GetCompositor(compositor_id);
if (!compositor)
return; // It's legal to race against GFX's compositor setup.
gfx::LayerStackPtr layer_stack = compositor->layer_stack();
if (!layer_stack)
return; // It's legal to race against GFX's layer stack setup.
DispatchCommand(input.send_pointer_input());
} else if (input.is_set_hard_keyboard_delivery()) {
DispatchCommand(input.set_hard_keyboard_delivery());
} else if (input.is_set_parallel_dispatch()) {
DispatchCommand(input.set_parallel_dispatch());
}
}
void InputCommandDispatcher::DispatchCommand(const SendPointerInputCmd& command) {
TRACE_DURATION("input", "dispatch_command", "command", "PointerCmd");
switch (command.pointer_event.type) {
case PointerEventType::TOUCH:
DispatchTouchCommand(command);
break;
case PointerEventType::MOUSE:
DispatchMouseCommand(command);
break;
default:
// TODO(SCN-940), TODO(SCN-164): Stylus support needs to account for HOVER
// events, which need to trigger an additional hit test on the DOWN event
// and send CANCEL events to disassociated clients.
FXL_LOG(INFO) << "Add stylus support.";
break;
}
}
// The touch state machine comprises ADD/DOWN/MOVE*/UP/REMOVE. Some notes:
// - We assume one touchscreen device, and use the device-assigned finger ID.
// - Touch ADD associates the following ADD/DOWN/MOVE*/UP/REMOVE event sequence
// with the set of clients available at that time. To enable gesture
// disambiguation, we perform parallel dispatch to all clients.
// - Touch DOWN triggers a focus change, honoring the "may receive focus" property.
// - Touch REMOVE drops the association between event stream and client.
void InputCommandDispatcher::DispatchTouchCommand(const SendPointerInputCmd& command) {
TRACE_DURATION("input", "dispatch_command", "command", "TouchCmd");
trace_flow_id_t trace_id =
PointerTraceHACK(command.pointer_event.radius_major, command.pointer_event.radius_minor);
TRACE_FLOW_END("input", "dispatch_event_to_scenic", trace_id);
const uint32_t pointer_id = command.pointer_event.pointer_id;
const Phase pointer_phase = command.pointer_event.phase;
const escher::vec2 pointer = PointerCoords(command.pointer_event);
const bool a11y_enabled = ShouldForwardAccessibilityPointerEvents();
FXL_DCHECK(command.pointer_event.type == PointerEventType::TOUCH);
FXL_DCHECK(pointer_phase != Phase::HOVER) << "Oops, touch device had unexpected HOVER event.";
if (pointer_phase == Phase::ADD) {
GlobalId compositor_id(session_id_, command.compositor_id);
gfx::SessionHitAccumulator accumulator;
PerformGlobalHitTest(GetLayerStack(engine_, compositor_id), pointer, &accumulator);
const auto& hits = accumulator.hits();
// Find input targets. Honor the "input masking" view property.
ViewStack hit_views;
{
// Find the transform (input -> view coordinates) for each hit and fill out hit_views.
for (const gfx::ViewHit& hit : hits) {
hit_views.stack.push_back({
hit.view->view_ref_koid(),
hit.view->event_reporter()->GetWeakPtr(),
hit.transform,
});
if (/*TODO(SCN-919): view_id may mask input */ false) {
break;
}
}
}
FXL_VLOG(1) << "View stack of hits: " << hit_views;
// Save targets for consistent delivery of touch events.
touch_targets_[pointer_id] = hit_views;
// If there is an accessibility pointer event listener enabled, an ADD event means that a new
// pointer id stream started. Perform it unconditionally, even if the view stack is empty.
if (a11y_enabled) {
pointer_event_buffer_->AddStream(pointer_id);
}
} else if (pointer_phase == Phase::DOWN) {
// If accessibility listener is on, focus change events must be sent only if
// the stream is rejected. This way, this operation is deferred.
if (!a11y_enabled) {
if (!touch_targets_[pointer_id].stack.empty()) {
// Request that focus be transferred to the top view.
RequestFocusChange(touch_targets_[pointer_id].stack[0].view_ref_koid);
} else if (focus_chain_root() != ZX_KOID_INVALID) {
// The touch event stream has no designated receiver.
// Request that focus be transferred to the root view, so that (1) the currently focused
// view becomes unfocused, and (2) the focus chain remains under control of the root view.
RequestFocusChange(focus_chain_root());
}
}
}
// Input delivery must be parallel; needed for gesture disambiguation.
std::vector<ViewStack::Entry> deferred_event_receivers;
for (const auto& entry : touch_targets_[pointer_id].stack) {
if (a11y_enabled) {
deferred_event_receivers.emplace_back(entry);
} else {
ReportPointerEvent(entry, command.pointer_event);
}
if (!parallel_dispatch_) {
break; // TODO(SCN-1047): Remove when gesture disambiguation is ready.
}
}
FXL_DCHECK(a11y_enabled || deferred_event_receivers.empty())
<< "When a11y pointer forwarding is off, never defer events.";
if (a11y_enabled) {
// We handle both latched (!deferred_event_receivers.empty()) and unlatched
// (deferred_event_receivers.empty()) touch events, for two reasons. (1) We must notify
// accessibility about events regardless of latch, so that it has full
// information about a gesture stream. E.g., the gesture could start traversal in empty
// space before MOVE-ing onto a rect; accessibility needs both the gesture and the rect.
// (2) We must trigger a potential focus change request, even if no view receives the triggering
// DOWN event, so that (a) the focused view receives an unfocus event, and (b) the focus
// chain gets updated and dispatched accordingly.
//
// NOTE: Do not rely on the latched view stack for "top hit" information; elevation can change
// dynamically (it's only guaranteed correct for DOWN). Instead, perform an independent query
// for "top hit".
glm::mat4 view_transform(1.f);
zx_koid_t view_ref_koid = ZX_KOID_INVALID;
GlobalId compositor_id(session_id_, command.compositor_id);
gfx::LayerStackPtr layer_stack = GetLayerStack(engine_, compositor_id);
{
// Find top-hit target and send it to accessibility.
// NOTE: We may hit various mouse cursors (owned by root presenter), but |TopHitAccumulator|
// will keep going until we find a hit with a valid owning View.
gfx::TopHitAccumulator top_hit;
PerformGlobalHitTest(layer_stack, pointer, &top_hit);
if (top_hit.hit()) {
const gfx::ViewHit& hit = *top_hit.hit();
view_transform = hit.transform;
view_ref_koid = hit.view->view_ref_koid();
}
}
const auto ndc = NormalizePointerCoords(pointer, layer_stack);
const auto top_hit_view_local = TransformPointerCoords(pointer, view_transform);
AccessibilityPointerEvent packet = BuildAccessibilityPointerEvent(
command.pointer_event, ndc, top_hit_view_local, view_ref_koid);
pointer_event_buffer_->AddEvent(
pointer_id,
{.event = std::move(command.pointer_event),
.parallel_event_receivers = std::move(deferred_event_receivers)},
std::move(packet));
} else {
input_system_->ReportPointerEventToPointerCaptureListener(command.pointer_event);
}
if (pointer_phase == Phase::REMOVE || pointer_phase == Phase::CANCEL) {
touch_targets_.erase(pointer_id);
}
}
// The mouse state machine is simpler, comprising MOVE*-DOWN/MOVE*/UP-MOVE*. Its
// behavior is similar to touch events, but with some differences.
// - There can be multiple mouse devices, so we track each device individually.
// - Mouse DOWN associates the following DOWN/MOVE*/UP event sequence with one
// particular client: the top-hit View. Mouse events aren't associated with
// gestures, so there is no parallel dispatch.
// - Mouse DOWN triggers a focus change, honoring the "may receive focus" property.
// - Mouse UP drops the association between event stream and client.
// - For an unlatched MOVE event, we perform a hit test, and send the
// top-most client this MOVE event. Focus does not change for unlatched
// MOVEs.
// - The hit test must account for the mouse cursor itself, which today is
// owned by the root presenter. The nodes associated with visible mouse
// cursors(!) do not roll up to any View (as expected), but may appear in the
// hit test; our dispatch needs to account for such behavior.
// TODO(SCN-1078): Enhance trackpad support.
void InputCommandDispatcher::DispatchMouseCommand(const SendPointerInputCmd& command) {
TRACE_DURATION("input", "dispatch_command", "command", "MouseCmd");
const uint32_t device_id = command.pointer_event.device_id;
const Phase pointer_phase = command.pointer_event.phase;
const escher::vec2 pointer = PointerCoords(command.pointer_event);
FXL_DCHECK(command.pointer_event.type == PointerEventType::MOUSE);
FXL_DCHECK(pointer_phase != Phase::ADD && pointer_phase != Phase::REMOVE &&
pointer_phase != Phase::HOVER)
<< "Oops, mouse device (id=" << device_id << ") had an unexpected event: " << pointer_phase;
if (pointer_phase == Phase::DOWN) {
GlobalId compositor_id(session_id_, command.compositor_id);
// Find top-hit target and associated properties.
// NOTE: We may hit various mouse cursors (owned by root presenter), but |TopHitAccumulator|
// will keep going until we find a hit with a valid owning View.
gfx::TopHitAccumulator top_hit;
PerformGlobalHitTest(GetLayerStack(engine_, compositor_id), pointer, &top_hit);
ViewStack hit_view;
if (top_hit.hit()) {
const gfx::ViewHit& hit = *top_hit.hit();
hit_view.stack.push_back({
hit.view->view_ref_koid(),
hit.view->event_reporter()->GetWeakPtr(),
hit.transform,
});
}
FXL_VLOG(1) << "View hit: " << hit_view;
if (!hit_view.stack.empty()) {
// Request that focus be transferred to the top view.
RequestFocusChange(hit_view.stack[0].view_ref_koid);
} else if (focus_chain_root() != ZX_KOID_INVALID) {
// The mouse event stream has no designated receiver.
// Request that focus be transferred to the root view, so that (1) the currently focused view
// becomes unfocused, and (2) the focus chain remains under control of the root view.
RequestFocusChange(focus_chain_root());
}
// Save target for consistent delivery of mouse events.
mouse_targets_[device_id] = hit_view;
}
if (mouse_targets_.count(device_id) > 0 && // Tracking this device, and
mouse_targets_[device_id].stack.size() > 0) { // target view exists.
const auto& entry = mouse_targets_[device_id].stack[0];
PointerEvent clone;
fidl::Clone(command.pointer_event, &clone);
ReportPointerEvent(entry, std::move(clone));
}
if (pointer_phase == Phase::UP || pointer_phase == Phase::CANCEL) {
mouse_targets_.erase(device_id);
}
// Deal with unlatched MOVE events.
if (pointer_phase == Phase::MOVE && mouse_targets_.count(device_id) == 0) {
GlobalId compositor_id(session_id_, command.compositor_id);
// Find top-hit target and send it this move event.
// NOTE: We may hit various mouse cursors (owned by root presenter), but |TopHitAccumulator|
// will keep going until we find a hit with a valid owning View.
gfx::TopHitAccumulator top_hit;
PerformGlobalHitTest(GetLayerStack(engine_, compositor_id), pointer, &top_hit);
if (top_hit.hit()) {
const gfx::ViewHit& hit = *top_hit.hit();
ViewStack::Entry view_info;
view_info.reporter = hit.view->event_reporter()->GetWeakPtr();
view_info.transform = hit.transform;
PointerEvent clone;
fidl::Clone(command.pointer_event, &clone);
ReportPointerEvent(view_info, std::move(clone));
}
}
}
void InputCommandDispatcher::DispatchCommand(
const fuchsia::ui::input::SendKeyboardInputCmd& command) {
TRACE_DURATION("input", "dispatch_command", "command", "SendKeyboardInputCmd");
// Expected (but soon to be deprecated) event flow.
ReportToImeService(input_system_->ime_service(), command.keyboard_event);
// Unusual: Clients may have requested direct delivery when focused.
const zx_koid_t focused_view = focus();
if (focused_view == ZX_KOID_INVALID)
return; // No receiver.
const scenic_impl::gfx::ViewTree& view_tree = engine_->scene_graph()->view_tree();
EventReporterWeakPtr reporter = view_tree.EventReporterOf(focused_view);
scheduling::SessionId session_id = view_tree.SessionIdOf(focused_view);
if (reporter && input_system_->hard_keyboard_requested().count(session_id) > 0) {
ReportKeyboardEvent(reporter.get(), command.keyboard_event);
}
}
void InputCommandDispatcher::DispatchCommand(
const fuchsia::ui::input::SetHardKeyboardDeliveryCmd& command) {
// Can't easily retrieve owning view's ViewRef KOID from just the Session or SessionId.
FXL_VLOG(2) << "Hard keyboard events, session_id=" << session_id_
<< ", delivery_request=" << (command.delivery_request ? "on" : "off");
if (command.delivery_request) {
// Take this opportunity to remove dead sessions.
std::vector<scheduling::SessionId> dead_sessions;
for (auto& reporter : input_system_->hard_keyboard_requested()) {
if (!reporter.second) {
dead_sessions.push_back(reporter.first);
}
}
for (auto session_ids : dead_sessions) {
input_system_->hard_keyboard_requested().erase(session_id_);
}
// This code assumes one event reporter per session id.
FXL_DCHECK(input_system_->hard_keyboard_requested().count(session_id_) == 0);
if (event_reporter_)
input_system_->hard_keyboard_requested().insert({session_id_, event_reporter_->GetWeakPtr()});
} else {
input_system_->hard_keyboard_requested().erase(session_id_);
}
}
void InputCommandDispatcher::DispatchCommand(
const fuchsia::ui::input::SetParallelDispatchCmd& command) {
TRACE_DURATION("input", "dispatch_command", "command", "SetParallelDispatchCmd");
FXL_LOG(INFO) << "Scenic: Parallel dispatch is turned "
<< (command.parallel_dispatch ? "ON" : "OFF");
parallel_dispatch_ = command.parallel_dispatch;
}
void InputCommandDispatcher::DispatchDeferredPointerEvent(
PointerEventBuffer::DeferredPointerEvent views_and_event) {
// If this parallel dispatch of events corresponds to a DOWN event, this
// triggers a possible deferred focus change event.
if (views_and_event.event.phase == fuchsia::ui::input::PointerEventPhase::DOWN) {
if (!views_and_event.parallel_event_receivers.empty()) {
// Request that focus be transferred to the top view.
const zx_koid_t view_koid = views_and_event.parallel_event_receivers[0].view_ref_koid;
FXL_DCHECK(view_koid != ZX_KOID_INVALID) << "invariant";
RequestFocusChange(view_koid);
} else if (focus_chain_root() != ZX_KOID_INVALID) {
// The touch event stream has no designated receiver.
// Request that focus be transferred to the root view, so that (1) the currently focused
// view becomes unfocused, and (2) the focus chain remains under control of the root view.
RequestFocusChange(focus_chain_root());
}
}
input_system_->ReportPointerEventToPointerCaptureListener(views_and_event.event);
for (auto& view : views_and_event.parallel_event_receivers) {
ReportPointerEvent(view, views_and_event.event);
}
}
void InputCommandDispatcher::ReportPointerEvent(const ViewStack::Entry& view_info,
const PointerEvent& pointer) {
if (!view_info.reporter)
return; // Session's event reporter no longer available. Bail quietly.
TRACE_DURATION("input", "dispatch_event_to_client", "event_type", "pointer");
trace_flow_id_t trace_id = PointerTraceHACK(pointer.radius_major, pointer.radius_minor);
TRACE_FLOW_BEGIN("input", "dispatch_event_to_client", trace_id);
InputEvent event;
event.set_pointer(BuildLocalPointerEvent(pointer, view_info.transform));
view_info.reporter->EnqueueEvent(std::move(event));
}
void InputCommandDispatcher::ReportKeyboardEvent(EventReporter* reporter,
fuchsia::ui::input::KeyboardEvent keyboard) {
FXL_DCHECK(reporter) << "precondition";
InputEvent event;
event.set_keyboard(std::move(keyboard));
reporter->EnqueueEvent(std::move(event));
}
void InputCommandDispatcher::ReportToImeService(
const fuchsia::ui::input::ImeServicePtr& ime_service,
fuchsia::ui::input::KeyboardEvent keyboard) {
TRACE_DURATION("input", "dispatch_event_to_client", "event_type", "ime_keyboard_event");
if (ime_service && ime_service.is_bound()) {
InputEvent event;
event.set_keyboard(std::move(keyboard));
ime_service->InjectInput(std::move(event));
}
}
zx_koid_t InputCommandDispatcher::focus() const {
if (!engine_->scene_graph())
return ZX_KOID_INVALID; // No scene graph, no view tree, no focus chain.
const auto& chain = engine_->scene_graph()->view_tree().focus_chain();
if (chain.empty())
return ZX_KOID_INVALID; // Scene not present, or scene not connected to compositor.
const zx_koid_t focused_view = chain.back();
FXL_DCHECK(focused_view != ZX_KOID_INVALID) << "invariant";
return focused_view;
}
zx_koid_t InputCommandDispatcher::focus_chain_root() const {
if (!engine_->scene_graph())
return ZX_KOID_INVALID; // No scene graph, no view tree, no focus chain.
const auto& chain = engine_->scene_graph()->view_tree().focus_chain();
if (chain.empty())
return ZX_KOID_INVALID; // Scene not present, or scene not connected to compositor.
const zx_koid_t root_view = chain.front();
FXL_DCHECK(root_view != ZX_KOID_INVALID) << "invariant";
return root_view;
}
bool InputCommandDispatcher::ShouldForwardAccessibilityPointerEvents() {
if (input_system_->IsAccessibilityPointerEventForwardingEnabled()) {
// If the buffer was not initialized yet, perform the following sanity
// check: make sure to send active pointer event streams to their final
// location and do not send them to the a11y listener.
if (!pointer_event_buffer_) {
pointer_event_buffer_ = std::make_unique<PointerEventBuffer>(
/* DispatchEventFunction */
[this](PointerEventBuffer::DeferredPointerEvent views_and_event) {
DispatchDeferredPointerEvent(std::move(views_and_event));
},
/* ReportAccessibilityEventFunction */
[input_system = input_system_](fuchsia::ui::input::accessibility::PointerEvent pointer) {
input_system->accessibility_pointer_event_listener()->OnEvent(std::move(pointer));
});
for (const auto& kv : touch_targets_) {
// Force a reject in all active pointer IDs. When a new stream arrives,
// they will automatically be sent for the a11y listener decide
// what to do with them as the status will change to WAITING_RESPONSE.
pointer_event_buffer_->SetActiveStreamInfo(
/*pointer_id=*/kv.first, PointerEventBuffer::PointerIdStreamStatus::REJECTED);
}
// Registers an event handler for this listener. This callback captures a pointer to the event
// buffer that we own, so we need to clear it before we destroy it (see below).
input_system_->accessibility_pointer_event_listener().events().OnStreamHandled =
[buffer = pointer_event_buffer_.get()](
uint32_t device_id, uint32_t pointer_id,
fuchsia::ui::input::accessibility::EventHandling handled) {
buffer->UpdateStream(pointer_id, handled);
};
}
return true;
} else if (pointer_event_buffer_) {
// The listener disconnected. Release held events, delete the buffer.
input_system_->accessibility_pointer_event_listener().events().OnStreamHandled = nullptr;
pointer_event_buffer_.reset();
}
return false;
}
void InputCommandDispatcher::RequestFocusChange(zx_koid_t view) {
FXL_DCHECK(view != ZX_KOID_INVALID) << "precondition";
if (!engine_->scene_graph())
return; // No scene graph, no view tree, no focus chain.
if (engine_->scene_graph()->view_tree().focus_chain().empty())
return; // Scene not present, or scene not connected to compositor.
// Input system acts on authority of top-most view.
const zx_koid_t requestor = engine_->scene_graph()->view_tree().focus_chain()[0];
auto status = engine_->scene_graph()->RequestFocusChange(requestor, view);
FXL_VLOG(1) << "Scenic RequestFocusChange. Authority: " << requestor << ", request: " << view
<< ", status: " << static_cast<int>(status);
FXL_DCHECK(status == FocusChangeStatus::kAccept ||
status == FocusChangeStatus::kErrorRequestCannotReceiveFocus)
<< "User has authority to request focus change, but the only valid rejection is when the "
"requested view may not receive focus. Error code: "
<< static_cast<int>(status);
}
} // namespace input
} // namespace scenic_impl