| // 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 "garnet/examples/ui/hello_input/app.h" |
| |
| #include <limits> |
| #include <string> |
| |
| #include <fuchsia/math/cpp/fidl.h> |
| #include <lib/fdio/util.h> |
| #include <lib/fit/function.h> |
| #include <lib/zx/time.h> |
| |
| #include "lib/ui/input/cpp/formatting.h" |
| #include "lib/ui/scenic/cpp/resources.h" |
| |
| // Lifted from hello_views. |
| static fuchsia::sys::FileDescriptorPtr CloneFileDescriptor(int fd) { |
| zx_handle_t handles[FDIO_MAX_HANDLES] = {0, 0, 0}; |
| uint32_t types[FDIO_MAX_HANDLES] = { |
| ZX_HANDLE_INVALID, |
| ZX_HANDLE_INVALID, |
| ZX_HANDLE_INVALID, |
| }; |
| zx_status_t status = fdio_clone_fd(fd, 0, handles, types); |
| if (status <= 0) { |
| return nullptr; |
| } |
| fuchsia::sys::FileDescriptorPtr result = fuchsia::sys::FileDescriptor::New(); |
| result->type0 = types[0]; |
| result->handle0 = zx::handle(handles[0]); |
| result->type1 = types[1]; |
| result->handle1 = zx::handle(handles[1]); |
| result->type2 = types[2]; |
| result->handle2 = zx::handle(handles[2]); |
| return result; |
| } |
| |
| namespace examples_ui_hello_input { |
| |
| const uint32_t kNoFinger = std::numeric_limits<uint32_t>::max(); // Sentinel. |
| |
| App::App(async::Loop* loop) |
| : startup_context_(component::StartupContext::CreateFromStartupInfo()), |
| message_loop_(loop), |
| input_reader_(this), |
| next_device_token_(0), |
| focused_(false) { |
| FXL_DCHECK(startup_context_); |
| |
| scenic_ = startup_context_ |
| ->ConnectToEnvironmentService<fuchsia::ui::scenic::Scenic>(); |
| scenic_.set_error_handler([this](zx_status_t status) { OnScenicError(); }); |
| FXL_LOG(INFO) << "HelloInput - scenic connection"; |
| |
| session_ = std::make_unique<scenic::Session>(scenic_.get()); |
| session_->SetDebugName("Hello Input"); |
| session_->set_error_handler( |
| [this](zx_status_t status) { this->OnSessionError(); }); |
| session_->set_event_handler(fit::bind_member(this, &App::OnSessionEvents)); |
| FXL_LOG(INFO) << "HelloInput - session set up"; |
| |
| compositor_ = std::make_unique<scenic::DisplayCompositor>(session_.get()); |
| FXL_LOG(INFO) << "HelloInput - compositor set up"; |
| |
| input_reader_.Start(); |
| startup_context_->outgoing().AddPublicService( |
| input_device_registry_bindings_.GetHandler(this)); |
| FXL_LOG(INFO) << "HelloInput - 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(ZX_CLOCK_MONOTONIC)); |
| }); |
| |
| { |
| component::Services child_services; |
| fuchsia::sys::LaunchInfo launch_info; |
| launch_info.out = CloneFileDescriptor(STDOUT_FILENO); |
| launch_info.err = CloneFileDescriptor(STDERR_FILENO); |
| launch_info.url = |
| "fuchsia-pkg://fuchsia.com/hello_input_child#meta/" |
| "hello_input_child.cmx"; |
| launch_info.directory_request = child_services.NewRequest(); |
| startup_context_->launcher()->CreateComponent( |
| std::move(launch_info), child_controller_.NewRequest()); |
| child_services.ConnectToService(child_view_provider_.NewRequest(), |
| "view_provider"); |
| } |
| |
| { |
| 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)); |
| } |
| |
| FXL_LOG(INFO) << "HelloInput - 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) { |
| FXL_LOG(INFO) << "HelloInput - shutting down."; |
| child_controller_->Kill(); |
| async::PostTask(message_loop_->dispatcher(), |
| [this]() { OnSessionClose(); }); |
| } |
| } |
| } |
| |
| void App::OnScenicError() { |
| FXL_LOG(ERROR) << "HelloInput - scenic connection error."; |
| ReleaseSessionResources(); |
| message_loop_->Quit(); |
| } |
| |
| void App::OnSessionError() { |
| FXL_LOG(ERROR) << "HelloInput - session error."; |
| ReleaseSessionResources(); |
| message_loop_->Quit(); |
| } |
| |
| void App::OnSessionClose() { |
| FXL_LOG(INFO) << "HelloInput - 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: |
| FXL_LOG(FATAL) << "Unknown input event received."; |
| } |
| } else if (event.is_gfx()) { |
| FXL_LOG(ERROR) << "HelloInput - 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(SCN-920): 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); |
| FXL_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) { |
| FXL_LOG(INFO) << "HelloInput - 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, SCN-248. |
| 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); |
| |
| FXL_LOG(INFO) << "HelloInput - 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.AddPart(background); |
| |
| scene.AddChild(root_node); |
| FXL_LOG(INFO) << "HelloInput - root node is set up."; |
| } |
| |
| // Create View/ViewHolder. Attach ViewHolder to root node. |
| { |
| zx::eventpair view_token; |
| zx::eventpair view_holder_token; |
| zx_status_t status = |
| zx::eventpair::create(/*flags*/ 0u, &view_holder_token, &view_token); |
| FXL_CHECK(status == ZX_OK) |
| << "Failed to create eventpair for view/viewholder."; |
| |
| 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_); |
| FXL_LOG(INFO) << "HelloInput - 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); |
| FXL_LOG(INFO) << "HelloInput - 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->AddPart(top_bar); |
| |
| scenic::ShapeNode bottom_bar(session); |
| bottom_bar.SetTranslation(0.f, kTranslateY, kElevation); |
| bottom_bar.SetShape(horizontal_bar); |
| bottom_bar.SetMaterial(material); |
| frame->AddPart(bottom_bar); |
| |
| scenic::ShapeNode left_bar(session); |
| left_bar.SetTranslation(-kTranslateX, 0, kElevation); |
| left_bar.SetShape(vertical_bar); |
| left_bar.SetMaterial(material); |
| frame->AddPart(left_bar); |
| |
| scenic::ShapeNode right_bar(session); |
| right_bar.SetTranslation(kTranslateX, 0, kElevation); |
| right_bar.SetShape(vertical_bar); |
| right_bar.SetMaterial(material); |
| frame->AddPart(right_bar); |
| |
| focus_frame_ = std::move(frame); |
| FXL_LOG(INFO) << "HelloInput - 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); |
| } |
| |
| FXL_LOG(INFO) << "HelloInput - 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); |
| FXL_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_); |
| FXL_LOG(INFO) << "HelloInput - child view requested, view holder set up."; |
| } |
| } |
| |
| void App::OnDeviceSensorEvent(uint32_t device_id, |
| fuchsia::ui::input::InputReport event) { |
| FXL_VLOG(3) << "HelloInput - OnDeviceSensorEvent(device_id=" << device_id |
| << "): " << event; |
| } |
| |
| void App::OnDeviceInputEvent(uint32_t compositor_id, |
| fuchsia::ui::input::InputEvent event) { |
| FXL_VLOG(1) << "HelloInput - 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_; |
| |
| FXL_VLOG(2) << "HelloInput - RegisterDevice: " << device_id << " " |
| << descriptor; |
| std::unique_ptr<mozart::InputDeviceImpl> input_device = |
| std::make_unique<mozart::InputDeviceImpl>( |
| device_id, std::move(descriptor), std::move(input_device_request), |
| this); |
| |
| std::unique_ptr<mozart::DeviceState> state; |
| if (input_device->descriptor()->sensor) { |
| mozart::OnSensorEventCallback callback = |
| [this](uint32_t device_id, fuchsia::ui::input::InputReport event) { |
| OnDeviceSensorEvent(device_id, std::move(event)); |
| }; |
| state = std::make_unique<mozart::DeviceState>( |
| input_device->id(), input_device->descriptor(), std::move(callback)); |
| } else { |
| uint32_t compositor_id = compositor_->id(); // Input destination. |
| mozart::OnEventCallback callback = |
| [this, compositor_id](fuchsia::ui::input::InputEvent event) { |
| OnDeviceInputEvent(compositor_id, std::move(event)); |
| }; |
| state = std::make_unique<mozart::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(mozart::InputDeviceImpl* input_device) { |
| const uint32_t device_id = input_device->id(); |
| |
| if (device_by_id_.count(device_id) == 0) |
| return; |
| |
| FXL_VLOG(2) << "HelloInput - 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(mozart::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; |
| |
| FXL_VLOG(3) << "HelloInput - OnReport: " << device_id << " " << report; |
| |
| mozart::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 examples_ui_hello_input |