| // 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/examples/lab/direct_input/app.h" | |
| #include <fuchsia/math/cpp/fidl.h> | |
| #include <lib/fdio/fd.h> | |
| #include <lib/fit/function.h> | |
| #include <lib/fostr/fidl/fuchsia/ui/input/formatting.h> | |
| #include <lib/sys/cpp/file_descriptor.h> | |
| #include <lib/ui/scenic/cpp/resources.h> | |
| #include <lib/ui/scenic/cpp/view_token_pair.h> | |
| #include <lib/zx/time.h> | |
| #include <unistd.h> | |
| #include <zircon/processargs.h> | |
| #include <limits> | |
| #include <string> | |
| #include <utility> | |
| namespace direct_input { | |
| const uint32_t kNoFinger = std::numeric_limits<uint32_t>::max(); // Sentinel. | |
| App::App(async::Loop* loop) | |
| : component_context_(sys::ComponentContext::CreateAndServeOutgoingDirectory()), | |
| message_loop_(loop), | |
| input_reader_(this), | |
| next_device_token_(0), | |
| focused_(false) { | |
| FX_DCHECK(component_context_); | |
| scenic_ = component_context_->svc()->Connect<fuchsia::ui::scenic::Scenic>(); | |
| scenic_.set_error_handler([this](zx_status_t status) { OnScenicError(); }); | |
| FX_LOGS(INFO) << "DirectInput - scenic connection"; | |
| session_ = std::make_unique<scenic::Session>(scenic_.get()); | |
| session_->SetDebugName("Direct Input"); | |
| session_->set_error_handler([this](zx_status_t status) { this->OnSessionError(); }); | |
| session_->set_event_handler(fit::bind_member(this, &App::OnSessionEvents)); | |
| FX_LOGS(INFO) << "DirectInput - session set up"; | |
| compositor_ = std::make_unique<scenic::DisplayCompositor>(session_.get()); | |
| FX_LOGS(INFO) << "DirectInput - compositor set up"; | |
| input_reader_.Start(); | |
| component_context_->outgoing()->AddPublicService( | |
| input_device_registry_bindings_.GetHandler(this)); | |
| FX_LOGS(INFO) << "DirectInput - input set up (Press ESC to quit)."; | |
| scenic_->GetDisplayInfo([this](fuchsia::ui::gfx::DisplayInfo display_info) { | |
| CreateScene(display_info.width_in_px, display_info.height_in_px); | |
| UpdateScene(zx_clock_get_monotonic()); | |
| }); | |
| { | |
| fuchsia::sys::LaunchInfo launch_info; | |
| launch_info.out = sys::CloneFileDescriptor(STDOUT_FILENO); | |
| launch_info.err = sys::CloneFileDescriptor(STDERR_FILENO); | |
| launch_info.url = | |
| "fuchsia-pkg://fuchsia.com/direct_input_child#meta/" | |
| "direct_input_child.cmx"; | |
| auto services = sys::ServiceDirectory::CreateWithRequest(&launch_info.directory_request); | |
| fuchsia::sys::LauncherSyncPtr launcher; | |
| component_context_->svc()->Connect(launcher.NewRequest()); | |
| launcher->CreateComponent(std::move(launch_info), child_controller_.NewRequest()); | |
| services->Connect(child_view_provider_.NewRequest()); | |
| } | |
| { | |
| fuchsia::ui::input::SetHardKeyboardDeliveryCmd cmd; | |
| cmd.delivery_request = true; | |
| fuchsia::ui::input::Command input_cmd; | |
| input_cmd.set_set_hard_keyboard_delivery(std::move(cmd)); | |
| session_->Enqueue(std::move(input_cmd)); | |
| } | |
| FX_LOGS(INFO) << "DirectInput - child set up"; | |
| } | |
| App::~App() { ReleaseSessionResources(); } | |
| void App::ReleaseSessionResources() { | |
| if (session_) { | |
| compositor_.reset(); | |
| camera_.reset(); | |
| focus_frame_.reset(); | |
| for (size_t i = 0; i < 10; ++i) { | |
| pointer_id_[i] = kNoFinger; | |
| pointer_tracker_[i].reset(); | |
| } | |
| view_.reset(); | |
| view_holder_.reset(); | |
| child_view_holder_.reset(); | |
| session_->Flush(); | |
| session_.reset(); | |
| } | |
| } | |
| void App::CheckQuit(const fuchsia::ui::input::InputEvent& event) { | |
| if (event.is_keyboard()) { | |
| fuchsia::ui::input::KeyboardEvent key_event = event.keyboard(); | |
| if (key_event.hid_usage == /* Esc key */ 0x29 && | |
| key_event.phase == fuchsia::ui::input::KeyboardEventPhase::RELEASED) { | |
| FX_LOGS(INFO) << "DirectInput - shutting down."; | |
| child_controller_->Kill(); | |
| async::PostTask(message_loop_->dispatcher(), [this]() { OnSessionClose(); }); | |
| } | |
| } | |
| } | |
| void App::OnScenicError() { | |
| FX_LOGS(ERROR) << "DirectInput - scenic connection error."; | |
| ReleaseSessionResources(); | |
| message_loop_->Quit(); | |
| } | |
| void App::OnSessionError() { | |
| FX_LOGS(ERROR) << "DirectInput - session error."; | |
| ReleaseSessionResources(); | |
| message_loop_->Quit(); | |
| } | |
| void App::OnSessionClose() { | |
| FX_LOGS(INFO) << "DirectInput - session close."; | |
| ReleaseSessionResources(); | |
| message_loop_->Quit(); | |
| } | |
| void App::OnSessionEvents(std::vector<fuchsia::ui::scenic::Event> events) { | |
| using InputEvent = fuchsia::ui::input::InputEvent; | |
| using InputType = fuchsia::ui::input::InputEvent::Tag; | |
| for (const auto& event : events) { | |
| if (event.is_input()) { | |
| const InputEvent& input_event = event.input(); | |
| switch (input_event.Which()) { | |
| case InputType::kPointer: | |
| OnPointerEvent(input_event.pointer()); | |
| continue; | |
| case InputType::kKeyboard: | |
| OnKeyboardEvent(input_event.keyboard()); | |
| continue; | |
| case InputType::kFocus: | |
| OnFocusEvent(input_event.focus()); | |
| continue; | |
| case InputType::Invalid: | |
| FX_LOGS(FATAL) << "Unknown input event received."; | |
| } | |
| } else if (event.is_gfx()) { | |
| FX_LOGS(ERROR) << "DirectInput - GFX command unimplemented."; | |
| } | |
| } | |
| } | |
| void App::OnFocusEvent(const fuchsia::ui::input::FocusEvent& event) { | |
| focused_ = event.focused; | |
| if (focused_) { | |
| view_->AddChild(*focus_frame_); | |
| } else { | |
| view_->DetachChild(*focus_frame_); | |
| } | |
| } | |
| void App::OnKeyboardEvent(const fuchsia::ui::input::KeyboardEvent& event) { | |
| using Phase = fuchsia::ui::input::KeyboardEventPhase; | |
| // "Blink" the focus frame to acknowledge keyboard event. | |
| if (event.phase == Phase::PRESSED) { | |
| view_->DetachChild(*focus_frame_); | |
| async::PostDelayedTask( | |
| message_loop_->dispatcher(), | |
| [this]() { | |
| if (focused_) { | |
| view_->AddChild(*focus_frame_); | |
| } | |
| }, | |
| zx::msec(80)); | |
| } | |
| } | |
| // Helper function for OnPointerEvent. Find index of matching element. | |
| template <std::size_t N> | |
| static size_t find_idx(const std::array<uint32_t, N>& array, uint32_t elem) { | |
| for (size_t i = 0; i < N; ++i) { | |
| if (array[i] == elem) { | |
| return i; | |
| } | |
| } | |
| return kNoFinger; // Keep it simple. | |
| } | |
| // Helper function for OnPointerEvent. Return contents of array as a string. | |
| template <std::size_t N> | |
| static std::string contents(const std::array<uint32_t, N>& array) { | |
| std::string value = "["; | |
| for (size_t i = 0; i < N; ++i) { | |
| value += std::to_string(array[i]); | |
| if (i < N - 1) { | |
| value += ", "; | |
| } | |
| } | |
| return value + "]"; | |
| } | |
| // This function implements a very specific input-recognition behavior. | |
| // Despite parallel dispatch of input events on a DOWN hit, we only track a | |
| // pointer if we are also focused. In contrast, gestures need to see all inputs, | |
| // regardless of focus state. | |
| void App::OnPointerEvent(const fuchsia::ui::input::PointerEvent& event) { | |
| using Type = fuchsia::ui::input::PointerEventType; | |
| using Phase = fuchsia::ui::input::PointerEventPhase; | |
| if (event.type == Type::TOUCH) { | |
| // TODO(fxbug.dev/24137): Reduce the very noticeable tracking lag. | |
| if (focused_ && event.phase == Phase::DOWN) { | |
| // Nice to meet you. Add to known-fingers list. | |
| size_t idx = find_idx(pointer_id_, kNoFinger); | |
| FX_CHECK(idx != kNoFinger) << "Pointer index full: " << contents(pointer_id_); | |
| pointer_id_[idx] = event.pointer_id; | |
| view_->AddChild(*pointer_tracker_[idx]); | |
| pointer_tracker_[idx]->SetTranslation(event.x, event.y, -400.f); | |
| } else if (event.phase == Phase::MOVE) { | |
| size_t idx = find_idx(pointer_id_, event.pointer_id); | |
| if (idx != kNoFinger) { | |
| // It's a finger we know, keep moving. | |
| pointer_tracker_[idx]->SetTranslation(event.x, event.y, -400.f); | |
| } | |
| } else if (event.phase == Phase::UP || event.phase == Phase::CANCEL) { | |
| size_t idx = find_idx(pointer_id_, event.pointer_id); | |
| if (idx != kNoFinger) { | |
| // It's a finger we know, but time to remove. | |
| view_->DetachChild(*pointer_tracker_[idx]); | |
| pointer_id_[idx] = kNoFinger; | |
| } | |
| } | |
| } | |
| } | |
| void App::UpdateScene(uint64_t next_presentation_time) { | |
| session_->Present(next_presentation_time, [this](fuchsia::images::PresentationInfo info) { | |
| UpdateScene(info.presentation_time + 2 * info.presentation_interval); | |
| }); | |
| } | |
| void App::CreateScene(float display_width, float display_height) { | |
| FX_LOGS(INFO) << "DirectInput - display size: " << display_width << ", " << display_height; | |
| const float kMargin = 100.f; | |
| const float kRootWidth = display_width - 2.f * kMargin; | |
| const float kRootHeight = display_height - 2.f * kMargin; | |
| width_in_px_ = display_width; // Store display size, not view size! | |
| height_in_px_ = display_height; | |
| auto session = session_.get(); | |
| scenic::LayerStack layer_stack(session); | |
| compositor_->SetLayerStack(layer_stack); | |
| // Set up scene. | |
| scenic::Scene scene(session); | |
| { | |
| camera_ = std::make_unique<scenic::Camera>(scene); | |
| scenic::Renderer renderer(session); | |
| renderer.SetCamera(camera_->id()); | |
| scenic::Layer layer(session); | |
| layer.SetRenderer(renderer); | |
| layer.SetSize(display_width, display_height); // Need screen size, fxbug.dev/23494. | |
| layer_stack.AddLayer(layer); | |
| scenic::AmbientLight ambient_light(session); | |
| ambient_light.SetColor(0.3f, 0.3f, 0.3f); | |
| scene.AddLight(ambient_light); | |
| scenic::DirectionalLight directional_light(session); | |
| directional_light.SetColor(0.7f, 0.7f, 0.7f); | |
| directional_light.SetDirection(1.f, 1.f, -2.f); | |
| scene.AddLight(directional_light); | |
| FX_LOGS(INFO) << "DirectInput - scene is set up."; | |
| } | |
| // Set up root node, its dimensions, add green background. | |
| scenic::EntityNode root_node(session); | |
| { | |
| const float kElevation = 10.f; | |
| root_node.SetLabel("root_node"); | |
| root_node.SetClip(0, true); | |
| root_node.SetTranslation(display_width * 0.5f, display_height * 0.5f, -kElevation); | |
| scenic::ShapeNode background(session); | |
| scenic::Rectangle shape(session, kRootWidth, kRootHeight); | |
| background.SetShape(shape); | |
| scenic::Material material(session); | |
| material.SetColor(0, 255, 0, 255); // Green | |
| background.SetMaterial(material); | |
| root_node.AddChild(background); | |
| scene.AddChild(root_node); | |
| FX_LOGS(INFO) << "DirectInput - root node is set up."; | |
| } | |
| // Create View/ViewHolder. Attach ViewHolder to root node. | |
| { | |
| auto [view_token, view_holder_token] = scenic::ViewTokenPair::New(); | |
| view_holder_ = | |
| std::make_unique<scenic::ViewHolder>(session, std::move(view_holder_token), "view_holder"); | |
| view_holder_->SetLabel("main view_holder"); | |
| view_ = std::make_unique<scenic::View>(session, std::move(view_token), "view"); | |
| view_->SetLabel("main view"); | |
| root_node.Attach(*view_holder_); | |
| FX_LOGS(INFO) << "DirectInput - View/ViewHolder pair created."; | |
| } | |
| const float kMainWidth = display_width - 4.f * kMargin; | |
| const float kMainHeight = display_height - 4.f * kMargin; | |
| // Create main node, attach to View. Main node is accessible only from View. | |
| { | |
| const float kElevation = 20.f; | |
| scenic::ShapeNode node(session); | |
| node.SetLabel("main node"); | |
| scenic::Rectangle shape(session, kMainWidth, kMainHeight); | |
| node.SetShape(shape); | |
| scenic::Material material(session); | |
| material.SetColor(255, 0, 255, 255); // Fuchsia | |
| node.SetMaterial(material); | |
| node.SetTranslation(0.f, 0.f, -kElevation); | |
| view_->AddChild(node); | |
| FX_LOGS(INFO) << "DirectInput - main node added to view."; | |
| } | |
| // Create frame to trigger on focus to main node. | |
| { | |
| std::unique_ptr<scenic::EntityNode> frame = std::make_unique<scenic::EntityNode>(session); | |
| frame->SetLabel("focus frame"); | |
| const float kElevation = 30.f; // Z height | |
| const float kBar = 50.f; // bar thickness | |
| const float kTranslateX = (kMainWidth - kBar) * 0.5f; | |
| const float kTranslateY = (kMainHeight - kBar) * 0.5f; | |
| scenic::Material material(session); | |
| material.SetColor(128, 0, 128, 255); // Purple | |
| scenic::Rectangle horizontal_bar(session, kMainWidth, kBar); | |
| scenic::Rectangle vertical_bar(session, kBar, kMainHeight); | |
| scenic::ShapeNode top_bar(session); | |
| top_bar.SetTranslation(0.f, -kTranslateY, -kElevation); | |
| top_bar.SetShape(horizontal_bar); | |
| top_bar.SetMaterial(material); | |
| frame->AddChild(top_bar); | |
| scenic::ShapeNode bottom_bar(session); | |
| bottom_bar.SetTranslation(0.f, kTranslateY, -kElevation); | |
| bottom_bar.SetShape(horizontal_bar); | |
| bottom_bar.SetMaterial(material); | |
| frame->AddChild(bottom_bar); | |
| scenic::ShapeNode left_bar(session); | |
| left_bar.SetTranslation(-kTranslateX, 0, -kElevation); | |
| left_bar.SetShape(vertical_bar); | |
| left_bar.SetMaterial(material); | |
| frame->AddChild(left_bar); | |
| scenic::ShapeNode right_bar(session); | |
| right_bar.SetTranslation(kTranslateX, 0, -kElevation); | |
| right_bar.SetShape(vertical_bar); | |
| right_bar.SetMaterial(material); | |
| frame->AddChild(right_bar); | |
| focus_frame_ = std::move(frame); | |
| FX_LOGS(INFO) << "DirectInput - focus frame prepared."; | |
| } | |
| // Create a visual tracker for pointer movement. | |
| { | |
| const float kElevation = 400.f; | |
| for (size_t i = 0; i < 10; ++i) { | |
| pointer_id_[i] = kNoFinger; | |
| pointer_tracker_[i] = std::make_unique<scenic::ShapeNode>(session); | |
| pointer_tracker_[i]->SetLabel("pointer tracker"); | |
| pointer_tracker_[i]->SetTranslation(0.f, 0.f, -kElevation); | |
| scenic::Circle circle(session, 50.f); | |
| pointer_tracker_[i]->SetShape(circle); | |
| scenic::Material material(session); | |
| material.SetColor(128, 0, 128, 255); // Purple | |
| pointer_tracker_[i]->SetMaterial(material); | |
| } | |
| FX_LOGS(INFO) << "DirectInput - pointer tracker prepared."; | |
| } | |
| // Connect to child view, put it in a ViewHolder. | |
| { | |
| zx::eventpair view_holder; | |
| zx::eventpair view; | |
| zx_status_t status = zx::eventpair::create(0u, &view_holder, &view); | |
| FX_CHECK(status == ZX_OK); | |
| child_view_provider_->CreateView(std::move(view), nullptr, nullptr); | |
| child_view_holder_ = | |
| std::make_unique<scenic::ViewHolder>(session, std::move(view_holder), "child view holder"); | |
| child_view_holder_->SetLabel("child_view_holder"); | |
| root_node.Attach(*child_view_holder_); | |
| FX_LOGS(INFO) << "DirectInput - child view requested, view holder set up."; | |
| } | |
| } | |
| void App::OnDeviceSensorEvent(uint32_t device_id, fuchsia::ui::input::InputReport event) { | |
| FX_VLOGS(3) << "DirectInput - OnDeviceSensorEvent(device_id=" << device_id << "): " << event; | |
| } | |
| void App::OnDeviceInputEvent(uint32_t compositor_id, fuchsia::ui::input::InputEvent event) { | |
| FX_VLOGS(1) << "DirectInput - OnDeviceInputEvent: " << event; | |
| CheckQuit(event); | |
| fuchsia::ui::input::Command command; | |
| if (event.is_pointer()) { | |
| // Pointer events are tied to a particular compositor for routing. | |
| fuchsia::ui::input::SendPointerInputCmd cmd; | |
| cmd.pointer_event = std::move(event.pointer()); | |
| cmd.compositor_id = compositor_id; | |
| command.set_send_pointer_input(std::move(cmd)); | |
| } else if (event.is_keyboard()) { | |
| // Keyboard events are sent to a focused view, wherever that may be. | |
| fuchsia::ui::input::SendKeyboardInputCmd cmd; | |
| cmd.keyboard_event = std::move(event.keyboard()); | |
| cmd.compositor_id = compositor_id; | |
| command.set_send_keyboard_input(std::move(cmd)); | |
| } | |
| session_->Enqueue(std::move(command)); | |
| } | |
| void App::RegisterDevice( | |
| fuchsia::ui::input::DeviceDescriptor descriptor, | |
| fidl::InterfaceRequest<fuchsia::ui::input::InputDevice> input_device_request) { | |
| uint32_t device_id = ++next_device_token_; | |
| FX_VLOGS(2) << "DirectInput - RegisterDevice: " << device_id << " " << descriptor; | |
| std::unique_ptr<ui_input::InputDeviceImpl> input_device = | |
| std::make_unique<ui_input::InputDeviceImpl>(device_id, std::move(descriptor), | |
| std::move(input_device_request), this); | |
| std::unique_ptr<ui_input::DeviceState> state; | |
| if (input_device->descriptor()->sensor) { | |
| ui_input::OnSensorEventCallback callback = [this](uint32_t device_id, | |
| fuchsia::ui::input::InputReport event) { | |
| OnDeviceSensorEvent(device_id, std::move(event)); | |
| }; | |
| state = std::make_unique<ui_input::DeviceState>(input_device->id(), input_device->descriptor(), | |
| std::move(callback)); | |
| } else { | |
| uint32_t compositor_id = compositor_->id(); // Input destination. | |
| ui_input::OnEventCallback callback = [this, | |
| compositor_id](fuchsia::ui::input::InputEvent event) { | |
| OnDeviceInputEvent(compositor_id, std::move(event)); | |
| }; | |
| state = std::make_unique<ui_input::DeviceState>(input_device->id(), input_device->descriptor(), | |
| std::move(callback)); | |
| } | |
| state->OnRegistered(); | |
| device_by_id_.emplace(device_id, std::move(input_device)); | |
| device_state_by_id_.emplace(device_id, std::move(state)); | |
| } | |
| void App::OnDeviceDisconnected(ui_input::InputDeviceImpl* input_device) { | |
| const uint32_t device_id = input_device->id(); | |
| if (device_by_id_.count(device_id) == 0) | |
| return; | |
| FX_VLOGS(2) << "DirectInput - UnregisterDevice: " << device_id; | |
| device_state_by_id_[device_id]->OnUnregistered(); | |
| device_state_by_id_.erase(device_id); | |
| device_by_id_.erase(device_id); | |
| } | |
| void App::OnReport(ui_input::InputDeviceImpl* input_device, | |
| fuchsia::ui::input::InputReport report) { | |
| const uint32_t device_id = input_device->id(); | |
| if (device_by_id_.count(device_id) == 0) | |
| return; | |
| FX_VLOGS(3) << "DirectInput - OnReport: " << device_id << " " << report; | |
| ui_input::DeviceState* state = device_state_by_id_[device_id].get(); | |
| fuchsia::math::Size size; | |
| size.width = width_in_px_; | |
| size.height = height_in_px_; | |
| state->Update(std::move(report), size); | |
| } | |
| } // namespace direct_input |