blob: f79bc47dd3c8ed36dc032d2ffd60fba044bc905d [file] [log] [blame]
// Copyright 2022 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/a11y/lib/gesture_manager/gesture_manager_v2.h"
#include <fuchsia/ui/input/accessibility/cpp/fidl.h>
#include <lib/syslog/cpp/macros.h>
#include <cstdint>
#include <memory>
#include <optional>
#include "fuchsia/ui/pointer/cpp/fidl.h"
#include "src/ui/a11y/lib/gesture_manager/arena_v2/gesture_arena_v2.h"
namespace a11y {
namespace {
using fuchsia::ui::pointer::EventPhase;
using fuchsia::ui::pointer::Rectangle;
using fuchsia::ui::pointer::TouchResponse;
using fuchsia::ui::pointer::TouchResponseType;
using fuchsia::ui::pointer::augment::TouchEventWithLocalHit;
using fuchsia::ui::pointer::augment::TouchSourceWithLocalHitPtr;
// Convert a `TouchInteractionId` into a triple of `uint32_t`, so that we can
// use it as the key in a `std::set`.
std::tuple<uint32_t, uint32_t, uint32_t> interactionToTriple(
fuchsia::ui::pointer::TouchInteractionId interaction) {
return {interaction.device_id, interaction.pointer_id, interaction.interaction_id};
}
// Helper for `normalizeToNdc`.
//
// Normalize `p` to be in the square [0, 1] * [0, 1].
//
// Returns std::nullopt if p is not contained in bounds.
std::optional<std::array<float, 2>> normalizeToUnitSquare(std::array<float, 2> p,
Rectangle bounds) {
const float x = p[0];
const float y = p[1];
const float x_min = bounds.min[0];
const float y_min = bounds.min[1];
const float x_max = bounds.max[0];
const float y_max = bounds.max[1];
const bool in_bounds = (x_min <= x && x <= x_max) && (y_min <= y && y <= y_max);
if (!in_bounds) {
return std::nullopt;
}
const float width = x_max - x_min;
const float height = y_max - y_min;
const float dx = x - x_min;
const float dy = y - y_min;
return {{
width > 0 ? dx / width : 0,
height > 0 ? dy / height : 0,
}};
}
// Normalize `p` to be in the square [-1, 1] * [-1, 1].
//
// Returns std::nullopt if p is not contained in bounds.
std::optional<std::array<float, 2>> normalizeToNdc(std::array<float, 2> p, Rectangle bounds) {
auto normalized = normalizeToUnitSquare(p, bounds);
if (!normalized) {
return std::nullopt;
}
const float x = (*normalized)[0];
const float y = (*normalized)[1];
return {{
x * 2 - 1,
y * 2 - 1,
}};
}
// Based on the status of the current a11y gesture arena contest, how should we
// respond in the system-level gesture disambiguation.
//
// Note that this is only the initial response; sometimes we'll have to say
// "hold" to indicate we don't know whether this interaction is ours yet. Once
// the current a11y gesture arena contest completes, we go back and update our
// responses.
TouchResponseType initialResponse(InteractionTracker::ConsumptionStatus status, EventPhase phase) {
switch (status) {
case InteractionTracker::ConsumptionStatus::kAccept:
return TouchResponseType::YES_PRIORITIZE;
case InteractionTracker::ConsumptionStatus::kReject:
return TouchResponseType::NO;
case InteractionTracker::ConsumptionStatus::kUndecided:
switch (phase) {
case EventPhase::ADD:
case EventPhase::CHANGE:
return TouchResponseType::MAYBE_PRIORITIZE_SUPPRESS;
case EventPhase::REMOVE:
case EventPhase::CANCEL:
return TouchResponseType::HOLD_SUPPRESS;
}
}
}
// When a contest ends, any held interactions will have their responses updated.
//
// This simply translates from consumption status to response type.
TouchResponseType updatedResponse(InteractionTracker::ConsumptionStatus status) {
switch (status) {
case InteractionTracker::ConsumptionStatus::kUndecided:
FX_DCHECK(false) << "held interactions should only be updated when the contest is resolved";
return TouchResponseType::NO;
case InteractionTracker::ConsumptionStatus::kAccept:
return TouchResponseType::YES_PRIORITIZE;
case InteractionTracker::ConsumptionStatus::kReject:
return TouchResponseType::NO;
}
}
} // namespace
GestureManagerV2::GestureManagerV2(TouchSourceWithLocalHitPtr touch_source)
: GestureManagerV2(std::move(touch_source),
[](InteractionTracker::HeldInteractionCallback callback) {
return std::make_unique<GestureArenaV2>(std::move(callback));
}) {}
GestureManagerV2::GestureManagerV2(TouchSourceWithLocalHitPtr touch_source,
ArenaFactory arena_factory)
: touch_source_(std::move(touch_source)),
gesture_handler_([this](GestureRecognizerV2* recognizer) { AddRecognizer(recognizer); }) {
// Park a callback that will notify the TouchSource (via UpdateResponse) when
// a held interaction becomes decided.
auto callback = [this](fuchsia::ui::pointer::TouchInteractionId interaction,
uint64_t trace_flow_id, InteractionTracker::ConsumptionStatus status) {
FX_DCHECK(status != InteractionTracker::ConsumptionStatus::kUndecided);
// Held interactions mean different things between Scenic and A11y:
// A11y: a stream of pointer events started and ended, but no recognizer claimed them yet.
// 2. Scenic: a11y submitted a "HOLD" response to an open interaction.
//
// It can be the case that A11y had a held interaction that gets resolved before we tell Scenic
// to hold that interaction for us. This can happen because everything here is async: a11y,
// Scenic, recognizers, input.
//
// In summary: only update a held interaction with Scenic if we previously told Scenic to hold
// that one for us.
auto it = held_interactions_.find(interactionToTriple(interaction));
if (it != held_interactions_.end()) {
TouchResponse response;
response.set_response_type(updatedResponse(status));
response.set_trace_flow_id(trace_flow_id);
touch_source_->UpdateResponse(interaction, std::move(response), [](auto...) {});
held_interactions_.erase(it);
}
};
arena_ = arena_factory(std::move(callback));
FX_DCHECK(arena_);
WatchForTouchEvents({});
}
void GestureManagerV2::WatchForTouchEvents(std::vector<TouchResponse> old_responses) {
auto callback = [this](std::vector<TouchEventWithLocalHit> events) {
auto responses = HandleEvents(std::move(events));
WatchForTouchEvents(std::move(responses));
};
touch_source_->Watch(std::move(old_responses), callback);
}
std::vector<fuchsia::ui::pointer::TouchResponse> GestureManagerV2::HandleEvents(
std::vector<fuchsia::ui::pointer::augment::TouchEventWithLocalHit> events) {
std::vector<TouchResponse> responses;
for (uint32_t i = 0; i < events.size(); ++i) {
auto& event = events[i];
if (event.touch_event.has_view_parameters()) {
viewport_bounds_ = event.touch_event.view_parameters().viewport;
}
// Warning: the field name `position_in_viewport` will no longer make sense.
ConvertToNdc(event);
auto response = HandleEvent(event);
responses.push_back(std::move(response));
}
return responses;
}
fuchsia::ui::pointer::TouchResponse GestureManagerV2::HandleEvent(
const fuchsia::ui::pointer::augment::TouchEventWithLocalHit& event) {
if (!event.touch_event.has_pointer_sample()) {
// For non-sample events, the TouchSource API expects an empty response.
return {};
}
fuchsia::ui::pointer::TouchResponse response;
FX_DCHECK(event.touch_event.has_trace_flow_id());
response.set_trace_flow_id(event.touch_event.trace_flow_id());
FX_DCHECK(event.touch_event.pointer_sample().has_phase());
const auto contest_status = arena_->OnEvent(event);
const auto phase = event.touch_event.pointer_sample().phase();
const auto response_type = initialResponse(contest_status, phase);
if (response_type == TouchResponseType::HOLD_SUPPRESS) {
auto interaction_id = interactionToTriple(event.touch_event.pointer_sample().interaction());
held_interactions_.insert(interaction_id);
}
response.set_response_type(response_type);
return response;
}
void GestureManagerV2::ConvertToNdc(fuchsia::ui::pointer::augment::TouchEventWithLocalHit& event) {
if (!event.touch_event.has_pointer_sample() ||
!event.touch_event.pointer_sample().has_position_in_viewport()) {
return;
}
FX_CHECK(viewport_bounds_) << "received touch sample event before viewport bounds";
auto point = event.touch_event.mutable_pointer_sample()->mutable_position_in_viewport();
*point = *normalizeToNdc(*point, *viewport_bounds_);
}
void GestureManagerV2::AddRecognizer(GestureRecognizerV2* recognizer) { arena_->Add(recognizer); }
void GestureManagerV2::Clear() {
arena_->ClearRecognizers();
gesture_handler_.Clear();
held_interactions_.clear();
}
} // namespace a11y