| // Copyright 2022 The Flutter 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/embedder/touch_delegate.h" |
| |
| #include <lib/syslog/global.h> |
| #include <zircon/rights.h> |
| #include <zircon/status.h> |
| #include <zircon/types.h> |
| |
| #include "src/embedder/fuchsia_logger.h" |
| #include "src/embedder/pointer_utility.h" |
| |
| namespace fuchsia::ui::pointer { |
| // For using TouchInteractionId as a map key. |
| bool operator==(const fuchsia::ui::pointer::TouchInteractionId& a, |
| const fuchsia::ui::pointer::TouchInteractionId& b) { |
| return a.device_id == b.device_id && a.pointer_id == b.pointer_id && |
| a.interaction_id == b.interaction_id; |
| } |
| } // namespace fuchsia::ui::pointer |
| |
| namespace embedder { |
| namespace { |
| |
| /// Checks the validity of a TouchEvent's pointer sample |
| /// and returns if one exists |
| bool HasValidatedTouchSample(const fup::TouchEvent& event) { |
| if (!event.has_pointer_sample()) { |
| return false; |
| } |
| FX_DCHECK(event.pointer_sample().has_interaction()); |
| FX_DCHECK(event.pointer_sample().has_phase()); |
| FX_DCHECK(event.pointer_sample().has_position_in_viewport()); |
| return true; |
| } |
| |
| // Helper to insert one or two events into a vector buffer. |
| void InsertIntoBuffer(std::pair<FlutterPointerEvent, std::optional<FlutterPointerEvent>> events, |
| std::vector<FlutterPointerEvent>* buffer) { |
| FX_DCHECK(buffer); |
| buffer->emplace_back(std::move(events.first)); |
| if (events.second.has_value()) { |
| buffer->emplace_back(std::move(events.second.value())); |
| } |
| } |
| |
| /// Translates EventPhase to FlutterPointerPhase |
| FlutterPointerPhase GetChangeFromTouchEventPhase(fup::EventPhase phase) { |
| switch (phase) { |
| case fup::EventPhase::ADD: |
| return FlutterPointerPhase::kAdd; |
| case fup::EventPhase::CHANGE: |
| return FlutterPointerPhase::kMove; |
| case fup::EventPhase::REMOVE: |
| return FlutterPointerPhase::kRemove; |
| case fup::EventPhase::CANCEL: |
| return FlutterPointerPhase::kCancel; |
| default: |
| return FlutterPointerPhase::kCancel; |
| } |
| } |
| |
| /// TODO(benbergkamp) need to re-evaluate if this is best mechanism for |
| /// implementing multi-touch. |
| /// |
| /// The return value is used to populate the device field which should |
| /// uniquely identify a pointer. We pack the device_id into the same field |
| /// to retain uniqueness across multiple touch device. |
| /// |
| /// When migrating to embedder API, the device field was changed from 64 bits |
| /// to 32 bits. Therefore we are unable to pack the entire device_id and |
| /// pointer_id fields into this without losing any bits. We now need to |
| /// shift the IDs by only 16 bits so that we can fit their low bits into a |
| /// single 32 bit integer. The checks offer some comfort that we will not |
| /// be losing data by removing the high bits. |
| uint32_t PackFuchsiaDeviceIdAndPointerId(uint32_t fuchsia_device_id, uint32_t fuchsia_pointer_id) { |
| static constexpr uint32_t MASK = 0xFFFF0000; |
| FX_CHECK((fuchsia_device_id & MASK) == 0); // Ensure high bits are 0 & no loss of data |
| FX_CHECK((fuchsia_pointer_id & MASK) == 0); // Ensure high bits are 0 & no loss of data |
| return (((uint32_t)fuchsia_device_id) << 16) | (uint16_t)fuchsia_pointer_id; |
| } |
| |
| /// It returns a "draft" because the coordinates are logical. Later, view pixel |
| /// ratio is applied to obtain physical coordinates. |
| /// |
| /// The flutter pointerdata state machine has extra phases, which this function |
| /// synthesizes on the fly. Hence the return data is a flutter pointerdata, and |
| /// optionally a second one. |
| /// For example: <ADD, DOWN>, <MOVE, nullopt>, <UP, REMOVE>. |
| /// TODO(fxbug.dev/87074): Let PointerDataPacketConverter synthesize events. |
| /// |
| /// Flutter gestures expect a gesture to start within the logical view space, and |
| /// is not tolerant of floating point drift. This function coerces just the DOWN |
| /// event's coordinate to start within the logical view. |
| std::pair<FlutterPointerEvent, std::optional<FlutterPointerEvent>> CreateTouchDraft( |
| const fup::TouchEvent& event, const fup::ViewParameters& view_parameters) { |
| const auto& sample = event.pointer_sample(); |
| const auto& ixn = sample.interaction(); |
| |
| FlutterPointerEvent ptr; |
| pointer_utility::ResetFlutterPointerEvent(ptr); |
| ptr.timestamp = pointer_utility::ConvertEventTimeToMicroseconds(event.timestamp()); |
| ptr.phase = GetChangeFromTouchEventPhase(sample.phase()); |
| ptr.device_kind = FlutterPointerDeviceKind::kFlutterPointerDeviceKindTouch; |
| // Load Fuchsia's pointer ID onto Flutter's |device| field, and not the |
| // |pointer_identifier| field. The latter is written by |
| // PointerDataPacketConverter, to track individual gesture interactions. |
| ptr.device = PackFuchsiaDeviceIdAndPointerId(ixn.device_id, ixn.pointer_id); |
| // View parameters can change mid-interaction; apply transform on the fly. |
| auto logical = pointer_utility::ViewportToViewCoordinates( |
| sample.position_in_viewport(), view_parameters.viewport_to_view_transform); |
| ptr.x = logical[0]; // Not yet physical; adjusted in PlatformView. |
| ptr.y = logical[1]; // Not yet physical; adjusted in PlatformView. |
| |
| // Match Flutter pointer's state machine with synthesized events. |
| if (ptr.phase == FlutterPointerPhase::kAdd) { |
| FlutterPointerEvent down; |
| memcpy(&down, &ptr, sizeof(FlutterPointerEvent)); |
| down.phase = FlutterPointerPhase::kDown; |
| { // Ensure gesture recognition: DOWN starts in the logical view space. |
| auto [x, y] = pointer_utility::ClampToViewSpace(down.x, down.y, view_parameters); |
| down.x = x; |
| down.y = y; |
| } |
| return {std::move(ptr), std::move(down)}; |
| } else if (ptr.phase == FlutterPointerPhase::kRemove) { |
| FlutterPointerEvent up; |
| memcpy(&up, &ptr, sizeof(FlutterPointerEvent)); |
| up.phase = FlutterPointerPhase::kUp; |
| return {std::move(up), std::move(ptr)}; |
| } else { |
| return {std::move(ptr), std::nullopt}; |
| } |
| } |
| |
| } // namespace |
| |
| TouchDelegate::TouchDelegate(fuchsia::ui::pointer::TouchSourceHandle touch_source) |
| : touch_source_(touch_source.Bind()) { |
| if (touch_source_) { |
| touch_source_.set_error_handler([](zx_status_t status) { |
| FX_LOGF(ERROR, kLogTag, "TouchSource channel error: %s", zx_status_get_string(status)); |
| }); |
| } |
| } |
| |
| // Core logic of this class. |
| // Aim to keep state management in this function. |
| void TouchDelegate::WatchLoop(std::function<void(std::vector<FlutterPointerEvent>)> callback) { |
| if (touch_responder_) { |
| FX_LOG(ERROR, embedder::kLogTag, "TouchDelegate WatchLoop must only be called once"); |
| return; |
| } |
| |
| touch_responder_ = [this, callback](std::vector<fup::TouchEvent> events) { |
| std::vector<FlutterPointerEvent> to_client; |
| for (const fup::TouchEvent& event : events) { |
| pointer_utility::IssueInputTraceEvent(event); |
| fup::TouchResponse response; // Response per event, matched on event's index. |
| if (event.has_view_parameters()) { |
| touch_view_parameters_ = std::move(event.view_parameters()); |
| } |
| if (HasValidatedTouchSample(event)) { |
| const auto& sample = event.pointer_sample(); |
| auto ixn = sample.interaction(); |
| // New stream without gesture disambiguation result --> buffer it |
| if (sample.phase() == fup::EventPhase::ADD && !event.has_interaction_result()) { |
| touch_buffer_.emplace(ixn, std::vector<FlutterPointerEvent>()); |
| } |
| |
| auto drafts = CreateTouchDraft(event, touch_view_parameters_.value()); |
| if (touch_buffer_.count(ixn) > 0) { |
| InsertIntoBuffer(std::move(drafts), &touch_buffer_[ixn]); |
| } else { |
| InsertIntoBuffer(std::move(drafts), &to_client); |
| } |
| // For this simple client, always claim we want the gesture. |
| response.set_response_type(fup::TouchResponseType::YES); |
| } |
| if (event.has_interaction_result()) { |
| const auto& result = event.interaction_result(); |
| const auto& ixn = result.interaction; |
| if (result.status == fup::TouchInteractionStatus::GRANTED && touch_buffer_.count(ixn) > 0) { |
| FX_DCHECK(to_client.empty()); |
| to_client.insert(to_client.end(), touch_buffer_[ixn].begin(), touch_buffer_[ixn].end()); |
| } |
| touch_buffer_.erase(ixn); // Result seen, delete the buffer. |
| } |
| touch_responses_.push_back(std::move(response)); |
| } |
| callback(std::move(to_client)); |
| touch_source_->Watch(std::move(touch_responses_), |
| /*copy*/ touch_responder_); |
| touch_responses_.clear(); |
| }; |
| |
| touch_source_->Watch(std::move(touch_responses_), |
| /*copy*/ touch_responder_); |
| touch_responses_.clear(); |
| } |
| |
| } // namespace embedder |