blob: 8d419ff3d637a279e96d4d65acb48e27a1fda385 [file] [log] [blame]
// Copyright 2013 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 "flutter/shell/platform/fuchsia/flutter/accessibility_bridge.h"
#include <lib/inspect/cpp/inspector.h>
#include <lib/zx/process.h>
#include <zircon/status.h>
#include <zircon/types.h>
#include <deque>
#include "flutter/fml/logging.h"
#include "flutter/lib/ui/semantics/semantics_node.h"
#include "../runtime/dart/utils/root_inspect_node.h"
namespace flutter_runner {
namespace {
#if !FLUTTER_RELEASE
static constexpr char kTreeDumpInspectRootName[] = "semantic_tree_root";
// Converts flutter semantic node flags to a string representation.
std::string NodeFlagsToString(const flutter::SemanticsNode& node) {
std::string output;
if (node.HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
output += "kHasCheckedState|";
}
if (node.HasFlag(flutter::SemanticsFlags::kHasEnabledState)) {
output += "kHasEnabledState|";
}
if (node.HasFlag(flutter::SemanticsFlags::kHasImplicitScrolling)) {
output += "kHasImplicitScrolling|";
}
if (node.HasFlag(flutter::SemanticsFlags::kHasToggledState)) {
output += "kHasToggledState|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsButton)) {
output += "kIsButton|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsChecked)) {
output += "kIsChecked|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsEnabled)) {
output += "kIsEnabled|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsFocusable)) {
output += "kIsFocusable|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsFocused)) {
output += "kIsFocused|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsHeader)) {
output += "kIsHeader|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsHidden)) {
output += "kIsHidden|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsImage)) {
output += "kIsImage|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup)) {
output += "kIsInMutuallyExclusiveGroup|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsKeyboardKey)) {
output += "kIsKeyboardKey|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsLink)) {
output += "kIsLink|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
output += "kIsLiveRegion|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsObscured)) {
output += "kIsObscured|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsReadOnly)) {
output += "kIsReadOnly|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsSelected)) {
output += "kIsSelected|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsSlider)) {
output += "kIsSlider|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsTextField)) {
output += "kIsTextField|";
}
if (node.HasFlag(flutter::SemanticsFlags::kIsToggled)) {
output += "kIsToggled|";
}
if (node.HasFlag(flutter::SemanticsFlags::kNamesRoute)) {
output += "kNamesRoute|";
}
if (node.HasFlag(flutter::SemanticsFlags::kScopesRoute)) {
output += "kScopesRoute|";
}
return output;
}
// Converts flutter semantic node actions to a string representation.
std::string NodeActionsToString(const flutter::SemanticsNode& node) {
std::string output;
if (node.HasAction(flutter::SemanticsAction::kCopy)) {
output += "kCopy|";
}
if (node.HasAction(flutter::SemanticsAction::kCustomAction)) {
output += "kCustomAction|";
}
if (node.HasAction(flutter::SemanticsAction::kCut)) {
output += "kCut|";
}
if (node.HasAction(flutter::SemanticsAction::kDecrease)) {
output += "kDecrease|";
}
if (node.HasAction(flutter::SemanticsAction::kDidGainAccessibilityFocus)) {
output += "kDidGainAccessibilityFocus|";
}
if (node.HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) {
output += "kDidLoseAccessibilityFocus|";
}
if (node.HasAction(flutter::SemanticsAction::kDismiss)) {
output += "kDismiss|";
}
if (node.HasAction(flutter::SemanticsAction::kIncrease)) {
output += "kIncrease|";
}
if (node.HasAction(flutter::SemanticsAction::kLongPress)) {
output += "kLongPress|";
}
if (node.HasAction(
flutter::SemanticsAction::kMoveCursorBackwardByCharacter)) {
output += "kMoveCursorBackwardByCharacter|";
}
if (node.HasAction(
flutter::SemanticsAction::kMoveCursorBackwardByWordIndex)) {
output += "kMoveCursorBackwardByWordIndex|";
}
if (node.HasAction(flutter::SemanticsAction::kMoveCursorForwardByCharacter)) {
output += "kMoveCursorForwardByCharacter|";
}
if (node.HasAction(flutter::SemanticsAction::kMoveCursorForwardByWordIndex)) {
output += "kMoveCursorForwardByWordIndex|";
}
if (node.HasAction(flutter::SemanticsAction::kPaste)) {
output += "kPaste|";
}
if (node.HasAction(flutter::SemanticsAction::kScrollDown)) {
output += "kScrollDown|";
}
if (node.HasAction(flutter::SemanticsAction::kScrollLeft)) {
output += "kScrollLeft|";
}
if (node.HasAction(flutter::SemanticsAction::kScrollRight)) {
output += "kScrollRight|";
}
if (node.HasAction(flutter::SemanticsAction::kScrollUp)) {
output += "kScrollUp|";
}
if (node.HasAction(flutter::SemanticsAction::kSetSelection)) {
output += "kSetSelection|";
}
if (node.HasAction(flutter::SemanticsAction::kSetText)) {
output += "kSetText|";
}
if (node.HasAction(flutter::SemanticsAction::kShowOnScreen)) {
output += "kShowOnScreen|";
}
if (node.HasAction(flutter::SemanticsAction::kTap)) {
output += "kTap|";
}
return output;
}
// Returns a string representation of the flutter semantic node absolut
// location.
std::string NodeLocationToString(const SkRect& rect) {
auto min_x = rect.fLeft;
auto min_y = rect.fTop;
auto max_x = rect.fRight;
auto max_y = rect.fBottom;
std::string location =
"min(" + std::to_string(min_x) + ", " + std::to_string(min_y) + ") max(" +
std::to_string(max_x) + ", " + std::to_string(max_y) + ")";
return location;
}
// Returns a string representation of the node's different types of children.
std::string NodeChildrenToString(const flutter::SemanticsNode& node) {
std::stringstream output;
if (!node.childrenInTraversalOrder.empty()) {
output << "children in traversal order:[";
for (const auto child_id : node.childrenInTraversalOrder) {
output << child_id << ", ";
}
output << "]\n";
}
if (!node.childrenInHitTestOrder.empty()) {
output << "children in hit test order:[";
for (const auto child_id : node.childrenInHitTestOrder) {
output << child_id << ", ";
}
output << ']';
}
return output.str();
}
#endif // !FLUTTER_RELEASE
} // namespace
AccessibilityBridge::AccessibilityBridge(
SetSemanticsEnabledCallback set_semantics_enabled_callback,
DispatchSemanticsActionCallback dispatch_semantics_action_callback,
fidl::InterfaceHandle<fuchsia::accessibility::semantics::SemanticsManager>
semantics_manager,
fuchsia::ui::views::ViewRef view_ref,
inspect::Node inspect_node)
: set_semantics_enabled_callback_(
std::move(set_semantics_enabled_callback)),
dispatch_semantics_action_callback_(
std::move(dispatch_semantics_action_callback)),
binding_(this),
fuchsia_semantics_manager_(semantics_manager.Bind()),
inspect_node_(std::move(inspect_node)) {
fuchsia_semantics_manager_.set_error_handler([](zx_status_t status) {
FML_LOG(ERROR) << "Flutter cannot connect to SemanticsManager with status: "
<< zx_status_get_string(status) << ".";
});
fuchsia_semantics_manager_->RegisterViewForSemantics(
std::move(view_ref), binding_.NewBinding(), tree_ptr_.NewRequest());
#if !FLUTTER_RELEASE
// The first argument to |CreateLazyValues| is the name of the lazy node, and
// will only be displayed if the callback used to generate the node's content
// fails. Therefore, we use an error message for this node name.
inspect_node_tree_dump_ =
inspect_node_.CreateLazyValues("dump_fail", [this]() {
inspect::Inspector inspector;
if (auto it = nodes_.find(kRootNodeId); it == nodes_.end()) {
inspector.GetRoot().CreateString(
"empty_tree", "this semantic tree is empty", &inspector);
} else {
FillInspectTree(
kRootNodeId, /*current_level=*/1,
inspector.GetRoot().CreateChild(kTreeDumpInspectRootName),
&inspector);
}
return fpromise::make_ok_promise(std::move(inspector));
});
#endif // !FLUTTER_RELEASE
}
bool AccessibilityBridge::GetSemanticsEnabled() const {
return semantics_enabled_;
}
void AccessibilityBridge::SetSemanticsEnabled(bool enabled) {
semantics_enabled_ = enabled;
if (!enabled) {
nodes_.clear();
}
}
fuchsia::ui::gfx::BoundingBox AccessibilityBridge::GetNodeLocation(
const flutter::SemanticsNode& node) const {
fuchsia::ui::gfx::BoundingBox box;
box.min.x = node.rect.fLeft;
box.min.y = node.rect.fTop;
box.min.z = static_cast<float>(node.elevation);
box.max.x = node.rect.fRight;
box.max.y = node.rect.fBottom;
box.max.z = static_cast<float>(node.thickness);
return box;
}
fuchsia::ui::gfx::mat4 AccessibilityBridge::GetNodeTransform(
const flutter::SemanticsNode& node) const {
return ConvertSkiaTransformToMat4(node.transform);
}
fuchsia::ui::gfx::mat4 AccessibilityBridge::ConvertSkiaTransformToMat4(
const SkM44 transform) const {
fuchsia::ui::gfx::mat4 value;
float* m = value.matrix.data();
transform.getColMajor(m);
return value;
}
fuchsia::accessibility::semantics::Attributes
AccessibilityBridge::GetNodeAttributes(const flutter::SemanticsNode& node,
size_t* added_size) const {
fuchsia::accessibility::semantics::Attributes attributes;
// TODO(MI4-2531): Don't truncate.
if (node.label.size() > fuchsia::accessibility::semantics::MAX_LABEL_SIZE) {
attributes.set_label(node.label.substr(
0, fuchsia::accessibility::semantics::MAX_LABEL_SIZE));
*added_size += fuchsia::accessibility::semantics::MAX_LABEL_SIZE;
} else {
attributes.set_label(node.label);
*added_size += node.label.size();
}
if (node.tooltip.size() > fuchsia::accessibility::semantics::MAX_LABEL_SIZE) {
attributes.set_secondary_label(node.tooltip.substr(
0, fuchsia::accessibility::semantics::MAX_LABEL_SIZE));
*added_size += fuchsia::accessibility::semantics::MAX_LABEL_SIZE;
} else {
attributes.set_secondary_label(node.tooltip);
*added_size += node.tooltip.size();
}
if (node.HasFlag(flutter::SemanticsFlags::kIsKeyboardKey)) {
attributes.set_is_keyboard_key(true);
}
return attributes;
}
fuchsia::accessibility::semantics::States AccessibilityBridge::GetNodeStates(
const flutter::SemanticsNode& node,
size_t* additional_size) const {
fuchsia::accessibility::semantics::States states;
(*additional_size) += sizeof(fuchsia::accessibility::semantics::States);
// Set checked state.
if (!node.HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
states.set_checked_state(
fuchsia::accessibility::semantics::CheckedState::NONE);
} else {
states.set_checked_state(
node.HasFlag(flutter::SemanticsFlags::kIsChecked)
? fuchsia::accessibility::semantics::CheckedState::CHECKED
: fuchsia::accessibility::semantics::CheckedState::UNCHECKED);
}
// Set selected state.
states.set_selected(node.HasFlag(flutter::SemanticsFlags::kIsSelected));
// Flutter's definition of a hidden node is different from Fuchsia, so it must
// not be set here.
// Set value.
if (node.value.size() > fuchsia::accessibility::semantics::MAX_VALUE_SIZE) {
states.set_value(node.value.substr(
0, fuchsia::accessibility::semantics::MAX_VALUE_SIZE));
(*additional_size) += fuchsia::accessibility::semantics::MAX_VALUE_SIZE;
} else {
states.set_value(node.value);
(*additional_size) += node.value.size();
}
// Set toggled state.
if (node.HasFlag(flutter::SemanticsFlags::kHasToggledState)) {
states.set_toggled_state(
node.HasFlag(flutter::SemanticsFlags::kIsToggled)
? fuchsia::accessibility::semantics::ToggledState::ON
: fuchsia::accessibility::semantics::ToggledState::OFF);
}
return states;
}
std::vector<fuchsia::accessibility::semantics::Action>
AccessibilityBridge::GetNodeActions(const flutter::SemanticsNode& node,
size_t* additional_size) const {
std::vector<fuchsia::accessibility::semantics::Action> node_actions;
if (node.HasAction(flutter::SemanticsAction::kTap)) {
node_actions.push_back(fuchsia::accessibility::semantics::Action::DEFAULT);
}
if (node.HasAction(flutter::SemanticsAction::kLongPress)) {
node_actions.push_back(
fuchsia::accessibility::semantics::Action::SECONDARY);
}
if (node.HasAction(flutter::SemanticsAction::kShowOnScreen)) {
node_actions.push_back(
fuchsia::accessibility::semantics::Action::SHOW_ON_SCREEN);
}
if (node.HasAction(flutter::SemanticsAction::kIncrease)) {
node_actions.push_back(
fuchsia::accessibility::semantics::Action::INCREMENT);
}
if (node.HasAction(flutter::SemanticsAction::kDecrease)) {
node_actions.push_back(
fuchsia::accessibility::semantics::Action::DECREMENT);
}
*additional_size +=
node_actions.size() * sizeof(fuchsia::accessibility::semantics::Action);
return node_actions;
}
fuchsia::accessibility::semantics::Role AccessibilityBridge::GetNodeRole(
const flutter::SemanticsNode& node) const {
if (node.HasFlag(flutter::SemanticsFlags::kIsButton)) {
return fuchsia::accessibility::semantics::Role::BUTTON;
}
if (node.HasFlag(flutter::SemanticsFlags::kIsTextField)) {
return fuchsia::accessibility::semantics::Role::TEXT_FIELD;
}
if (node.HasFlag(flutter::SemanticsFlags::kIsLink)) {
return fuchsia::accessibility::semantics::Role::LINK;
}
if (node.HasFlag(flutter::SemanticsFlags::kIsSlider)) {
return fuchsia::accessibility::semantics::Role::SLIDER;
}
if (node.HasFlag(flutter::SemanticsFlags::kIsHeader)) {
return fuchsia::accessibility::semantics::Role::HEADER;
}
if (node.HasFlag(flutter::SemanticsFlags::kIsImage)) {
return fuchsia::accessibility::semantics::Role::IMAGE;
}
// If a flutter node supports the kIncrease or kDecrease actions, it can be
// treated as a slider control by assistive technology. This is important
// because users have special gestures to deal with sliders, and Fuchsia API
// requires nodes that can receive this kind of action to be a slider control.
if (node.HasAction(flutter::SemanticsAction::kIncrease) ||
node.HasAction(flutter::SemanticsAction::kDecrease)) {
return fuchsia::accessibility::semantics::Role::SLIDER;
}
// If a flutter node has a checked state, then we assume it is either a
// checkbox or a radio button. We distinguish between checkboxes and
// radio buttons based on membership in a mutually exclusive group.
if (node.HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
if (node.HasFlag(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup)) {
return fuchsia::accessibility::semantics::Role::RADIO_BUTTON;
} else {
return fuchsia::accessibility::semantics::Role::CHECK_BOX;
}
}
if (node.HasFlag(flutter::SemanticsFlags::kHasToggledState)) {
return fuchsia::accessibility::semantics::Role::TOGGLE_SWITCH;
}
return fuchsia::accessibility::semantics::Role::UNKNOWN;
}
std::unordered_set<int32_t> AccessibilityBridge::GetDescendants(
int32_t node_id) const {
std::unordered_set<int32_t> descendents;
std::deque<int32_t> to_process = {node_id};
while (!to_process.empty()) {
int32_t id = to_process.front();
to_process.pop_front();
descendents.emplace(id);
auto it = nodes_.find(id);
if (it != nodes_.end()) {
const auto& node = it->second.data;
for (const auto& child : node.childrenInHitTestOrder) {
if (descendents.find(child) == descendents.end()) {
to_process.push_back(child);
} else {
// This indicates either a cycle or a child with multiple parents.
// Flutter should never let this happen, but the engine API does not
// explicitly forbid it right now.
// TODO(http://fxbug.dev/75905): Crash flutter accessibility bridge
// when a cycle in the tree is found.
FML_LOG(ERROR) << "Semantics Node " << child
<< " has already been listed as a child of another "
"node, ignoring for parent "
<< id << ".";
}
}
}
}
return descendents;
}
// The only known usage of a negative number for a node ID is in the embedder
// API as a sentinel value, which is not expected here. No valid producer of
// nodes should give us a negative ID.
static uint32_t FlutterIdToFuchsiaId(int32_t flutter_node_id) {
FML_DCHECK(flutter_node_id >= 0)
<< "Unexpectedly received a negative semantics node ID.";
return static_cast<uint32_t>(flutter_node_id);
}
void AccessibilityBridge::PruneUnreachableNodes() {
const auto& reachable_nodes = GetDescendants(kRootNodeId);
std::vector<uint32_t> nodes_to_remove;
auto iter = nodes_.begin();
while (iter != nodes_.end()) {
int32_t id = iter->first;
if (reachable_nodes.find(id) == reachable_nodes.end()) {
// TODO(MI4-2531): This shouldn't be strictly necessary at this level.
if (sizeof(nodes_to_remove) + (nodes_to_remove.size() * kNodeIdSize) >=
kMaxMessageSize) {
tree_ptr_->DeleteSemanticNodes(std::move(nodes_to_remove));
nodes_to_remove.clear();
}
nodes_to_remove.push_back(FlutterIdToFuchsiaId(id));
iter = nodes_.erase(iter);
} else {
iter++;
}
}
if (!nodes_to_remove.empty()) {
tree_ptr_->DeleteSemanticNodes(std::move(nodes_to_remove));
}
}
// TODO(FIDL-718) - remove this, handle the error instead in something like
// set_error_handler.
static void PrintNodeSizeError(uint32_t node_id) {
FML_LOG(ERROR) << "Semantics node with ID " << node_id
<< " exceeded the maximum FIDL message size and may not "
"be delivered to the accessibility manager service.";
}
void AccessibilityBridge::AddSemanticsNodeUpdate(
const flutter::SemanticsNodeUpdates update,
float view_pixel_ratio) {
if (update.empty()) {
return;
}
FML_DCHECK(nodes_.find(kRootNodeId) != nodes_.end() ||
update.find(kRootNodeId) != update.end())
<< "AccessibilityBridge received an update with out ever getting a root "
"node.";
std::vector<fuchsia::accessibility::semantics::Node> fuchsia_nodes;
size_t current_size = 0;
bool has_root_node_update = false;
// TODO(MI4-2498): Actions, Roles, hit test children, additional
// flags/states/attr
// TODO(MI4-1478): Support for partial updates for nodes > 64kb
// e.g. if a node has a long label or more than 64k children.
for (const auto& [flutter_node_id, flutter_node] : update) {
size_t this_node_size = sizeof(fuchsia::accessibility::semantics::Node);
// We handle root update separately in GetRootNodeUpdate.
// TODO(chunhtai): remove this special case after we remove the inverse
// view pixel ratio transformation in scenic view.
// TODO(http://fxbug.dev/75908): Investigate flutter a11y bridge refactor
// after removal of the inverse view pixel ratio transformation in scenic
// view).
if (flutter_node.id == kRootNodeId) {
root_flutter_semantics_node_ = flutter_node;
has_root_node_update = true;
continue;
}
// Store the nodes for later hit testing and logging.
nodes_[flutter_node.id].data = flutter_node;
fuchsia::accessibility::semantics::Node fuchsia_node;
std::vector<uint32_t> child_ids;
// Send the nodes in traversal order, so the manager can figure out
// traversal.
for (int32_t flutter_child_id : flutter_node.childrenInTraversalOrder) {
child_ids.push_back(FlutterIdToFuchsiaId(flutter_child_id));
}
// TODO(http://fxbug.dev/75910): check the usage of FlutterIdToFuchsiaId in
// the flutter accessibility bridge.
fuchsia_node.set_node_id(flutter_node.id)
.set_role(GetNodeRole(flutter_node))
.set_location(GetNodeLocation(flutter_node))
.set_transform(GetNodeTransform(flutter_node))
.set_attributes(GetNodeAttributes(flutter_node, &this_node_size))
.set_states(GetNodeStates(flutter_node, &this_node_size))
.set_actions(GetNodeActions(flutter_node, &this_node_size))
.set_child_ids(child_ids);
this_node_size +=
kNodeIdSize * flutter_node.childrenInTraversalOrder.size();
// TODO(MI4-2531, FIDL-718): Remove this
// This is defensive. If, despite our best efforts, we ended up with a node
// that is larger than the max fidl size, we send no updates.
if (this_node_size >= kMaxMessageSize) {
PrintNodeSizeError(flutter_node.id);
return;
}
current_size += this_node_size;
// If we would exceed the max FIDL message size by appending this node,
// we should delete/update/commit now.
if (current_size >= kMaxMessageSize) {
tree_ptr_->UpdateSemanticNodes(std::move(fuchsia_nodes));
fuchsia_nodes.clear();
current_size = this_node_size;
}
fuchsia_nodes.push_back(std::move(fuchsia_node));
}
if (current_size > kMaxMessageSize) {
PrintNodeSizeError(fuchsia_nodes.back().node_id());
}
// Handles root node update.
if (has_root_node_update || last_seen_view_pixel_ratio_ != view_pixel_ratio) {
last_seen_view_pixel_ratio_ = view_pixel_ratio;
size_t root_node_size;
fuchsia::accessibility::semantics::Node root_update =
GetRootNodeUpdate(root_node_size);
// TODO(MI4-2531, FIDL-718): Remove this
// This is defensive. If, despite our best efforts, we ended up with a node
// that is larger than the max fidl size, we send no updates.
if (root_node_size >= kMaxMessageSize) {
PrintNodeSizeError(kRootNodeId);
return;
}
current_size += root_node_size;
// If we would exceed the max FIDL message size by appending this node,
// we should delete/update/commit now.
if (current_size >= kMaxMessageSize) {
tree_ptr_->UpdateSemanticNodes(std::move(fuchsia_nodes));
fuchsia_nodes.clear();
}
fuchsia_nodes.push_back(std::move(root_update));
}
PruneUnreachableNodes();
UpdateScreenRects();
tree_ptr_->UpdateSemanticNodes(std::move(fuchsia_nodes));
// TODO(dnfield): Implement the callback here
// https://bugs.fuchsia.dev/p/fuchsia/issues/detail?id=35718.
tree_ptr_->CommitUpdates([]() {});
}
fuchsia::accessibility::semantics::Node AccessibilityBridge::GetRootNodeUpdate(
size_t& node_size) {
fuchsia::accessibility::semantics::Node root_fuchsia_node;
std::vector<uint32_t> child_ids;
node_size = sizeof(fuchsia::accessibility::semantics::Node);
for (int32_t flutter_child_id :
root_flutter_semantics_node_.childrenInTraversalOrder) {
child_ids.push_back(FlutterIdToFuchsiaId(flutter_child_id));
}
// Applies the inverse view pixel ratio transformation to the root node.
float inverse_view_pixel_ratio = 1.f / last_seen_view_pixel_ratio_;
SkM44 inverse_view_pixel_ratio_transform;
inverse_view_pixel_ratio_transform.setScale(inverse_view_pixel_ratio,
inverse_view_pixel_ratio, 1.f);
SkM44 result = root_flutter_semantics_node_.transform *
inverse_view_pixel_ratio_transform;
nodes_[root_flutter_semantics_node_.id].data = root_flutter_semantics_node_;
// TODO(http://fxbug.dev/75910): check the usage of FlutterIdToFuchsiaId in
// the flutter accessibility bridge.
root_fuchsia_node.set_node_id(root_flutter_semantics_node_.id)
.set_role(GetNodeRole(root_flutter_semantics_node_))
.set_location(GetNodeLocation(root_flutter_semantics_node_))
.set_transform(ConvertSkiaTransformToMat4(result))
.set_attributes(
GetNodeAttributes(root_flutter_semantics_node_, &node_size))
.set_states(GetNodeStates(root_flutter_semantics_node_, &node_size))
.set_actions(GetNodeActions(root_flutter_semantics_node_, &node_size))
.set_child_ids(child_ids);
node_size += kNodeIdSize *
root_flutter_semantics_node_.childrenInTraversalOrder.size();
return root_fuchsia_node;
}
void AccessibilityBridge::RequestAnnounce(const std::string message) {
fuchsia::accessibility::semantics::SemanticEvent semantic_event;
fuchsia::accessibility::semantics::AnnounceEvent announce_event;
announce_event.set_message(message);
semantic_event.set_announce(std::move(announce_event));
tree_ptr_->SendSemanticEvent(std::move(semantic_event), []() {});
}
void AccessibilityBridge::UpdateScreenRects() {
std::unordered_set<int32_t> visited_nodes;
// The embedder applies a special pixel ratio transform to the root of the
// view, and the accessibility bridge applies the inverse of this transform
// to the root node. However, this transform is not persisted in the flutter
// representation of the root node, so we need to account for it explicitly
// here.
float inverse_view_pixel_ratio = 1.f / last_seen_view_pixel_ratio_;
SkM44 inverse_view_pixel_ratio_transform;
inverse_view_pixel_ratio_transform.setScale(inverse_view_pixel_ratio,
inverse_view_pixel_ratio, 1.f);
UpdateScreenRects(kRootNodeId, inverse_view_pixel_ratio_transform,
&visited_nodes);
}
void AccessibilityBridge::UpdateScreenRects(
int32_t node_id,
SkM44 parent_transform,
std::unordered_set<int32_t>* visited_nodes) {
auto it = nodes_.find(node_id);
if (it == nodes_.end()) {
FML_LOG(ERROR) << "UpdateScreenRects called on unknown node";
return;
}
auto& node = it->second;
const auto& current_transform = parent_transform * node.data.transform;
const auto& rect = node.data.rect;
SkV4 dst[2] = {
current_transform.map(rect.left(), rect.top(), 0, 1),
current_transform.map(rect.right(), rect.bottom(), 0, 1),
};
node.screen_rect.setLTRB(dst[0].x, dst[0].y, dst[1].x, dst[1].y);
node.screen_rect.sort();
visited_nodes->emplace(node_id);
for (uint32_t child_id : node.data.childrenInHitTestOrder) {
if (visited_nodes->find(child_id) == visited_nodes->end()) {
UpdateScreenRects(child_id, current_transform, visited_nodes);
}
}
}
std::optional<flutter::SemanticsAction>
AccessibilityBridge::GetFlutterSemanticsAction(
fuchsia::accessibility::semantics::Action fuchsia_action,
uint32_t node_id) {
switch (fuchsia_action) {
// The default action associated with the element.
case fuchsia::accessibility::semantics::Action::DEFAULT:
return flutter::SemanticsAction::kTap;
// The secondary action associated with the element. This may correspond to
// a long press (touchscreens) or right click (mouse).
case fuchsia::accessibility::semantics::Action::SECONDARY:
return flutter::SemanticsAction::kLongPress;
// Set (input/non-accessibility) focus on this element.
case fuchsia::accessibility::semantics::Action::SET_FOCUS:
FML_DLOG(WARNING)
<< "Unsupported action SET_FOCUS sent for accessibility node "
<< node_id;
return {};
// Set the element's value.
case fuchsia::accessibility::semantics::Action::SET_VALUE:
FML_DLOG(WARNING)
<< "Unsupported action SET_VALUE sent for accessibility node "
<< node_id;
return {};
// Scroll node to make it visible.
case fuchsia::accessibility::semantics::Action::SHOW_ON_SCREEN:
return flutter::SemanticsAction::kShowOnScreen;
case fuchsia::accessibility::semantics::Action::INCREMENT:
return flutter::SemanticsAction::kIncrease;
case fuchsia::accessibility::semantics::Action::DECREMENT:
return flutter::SemanticsAction::kDecrease;
default:
FML_DLOG(WARNING) << "Unexpected action "
<< static_cast<int32_t>(fuchsia_action)
<< " sent for accessibility node " << node_id;
return {};
}
}
// |fuchsia::accessibility::semantics::SemanticListener|
void AccessibilityBridge::OnAccessibilityActionRequested(
uint32_t node_id,
fuchsia::accessibility::semantics::Action action,
fuchsia::accessibility::semantics::SemanticListener::
OnAccessibilityActionRequestedCallback callback) {
// TODO(http://fxbug.dev/75910): check the usage of FlutterIdToFuchsiaId in
// the flutter accessibility bridge.
if (nodes_.find(node_id) == nodes_.end()) {
FML_LOG(ERROR) << "Attempted to send accessibility action "
<< static_cast<int32_t>(action)
<< " to unknown node id: " << node_id;
callback(false);
return;
}
std::optional<flutter::SemanticsAction> flutter_action =
GetFlutterSemanticsAction(action, node_id);
if (!flutter_action.has_value()) {
callback(false);
return;
}
dispatch_semantics_action_callback_(static_cast<int32_t>(node_id),
flutter_action.value());
callback(true);
}
// |fuchsia::accessibility::semantics::SemanticListener|
void AccessibilityBridge::HitTest(
fuchsia::math::PointF local_point,
fuchsia::accessibility::semantics::SemanticListener::HitTestCallback
callback) {
auto hit_node_id = GetHitNode(kRootNodeId, local_point.x, local_point.y);
FML_DCHECK(hit_node_id.has_value());
fuchsia::accessibility::semantics::Hit hit;
// TODO(http://fxbug.dev/75910): check the usage of FlutterIdToFuchsiaId in
// the flutter accessibility bridge.
hit.set_node_id(hit_node_id.value_or(kRootNodeId));
callback(std::move(hit));
}
std::optional<int32_t> AccessibilityBridge::GetHitNode(int32_t node_id,
float x,
float y) {
auto it = nodes_.find(node_id);
if (it == nodes_.end()) {
FML_LOG(ERROR) << "Attempted to hit test unknown node id: " << node_id;
return {};
}
auto const& node = it->second;
if (node.data.flags &
static_cast<int32_t>(flutter::SemanticsFlags::kIsHidden) || //
!node.screen_rect.contains(x, y)) {
return {};
}
for (int32_t child_id : node.data.childrenInHitTestOrder) {
auto candidate = GetHitNode(child_id, x, y);
if (candidate) {
return candidate;
}
}
if (IsFocusable(node.data)) {
return node_id;
}
return {};
}
bool AccessibilityBridge::IsFocusable(
const flutter::SemanticsNode& node) const {
if (node.HasFlag(flutter::SemanticsFlags::kScopesRoute)) {
return false;
}
if (node.HasFlag(flutter::SemanticsFlags::kIsFocusable)) {
return true;
}
// Always consider platform views focusable.
if (node.IsPlatformViewNode()) {
return true;
}
// Always consider actionable nodes focusable.
if (node.actions != 0) {
return true;
}
// Consider text nodes focusable.
return !node.label.empty() || !node.value.empty() || !node.hint.empty();
}
// |fuchsia::accessibility::semantics::SemanticListener|
void AccessibilityBridge::OnSemanticsModeChanged(
bool enabled,
OnSemanticsModeChangedCallback callback) {
set_semantics_enabled_callback_(enabled);
}
#if !FLUTTER_RELEASE
void AccessibilityBridge::FillInspectTree(int32_t flutter_node_id,
int32_t current_level,
inspect::Node inspect_node,
inspect::Inspector* inspector) const {
const auto it = nodes_.find(flutter_node_id);
if (it == nodes_.end()) {
inspect_node.CreateString(
"missing_child",
"This node has a parent in the semantic tree but has no value",
inspector);
inspector->emplace(std::move(inspect_node));
return;
}
const auto& semantic_node = it->second;
const auto& data = semantic_node.data;
inspect_node.CreateInt("id", data.id, inspector);
// Even with an empty label, we still want to create the property to
// explicetly show that it is empty.
inspect_node.CreateString("label", data.label, inspector);
if (!data.hint.empty()) {
inspect_node.CreateString("hint", data.hint, inspector);
}
if (!data.value.empty()) {
inspect_node.CreateString("value", data.value, inspector);
}
if (!data.increasedValue.empty()) {
inspect_node.CreateString("increased_value", data.increasedValue,
inspector);
}
if (!data.decreasedValue.empty()) {
inspect_node.CreateString("decreased_value", data.decreasedValue,
inspector);
}
if (data.textDirection) {
inspect_node.CreateString(
"text_direction", data.textDirection == 1 ? "RTL" : "LTR", inspector);
}
if (data.flags) {
inspect_node.CreateString("flags", NodeFlagsToString(data), inspector);
}
if (data.actions) {
inspect_node.CreateString("actions", NodeActionsToString(data), inspector);
}
inspect_node.CreateString(
"location", NodeLocationToString(semantic_node.screen_rect), inspector);
if (!data.childrenInTraversalOrder.empty() ||
!data.childrenInHitTestOrder.empty()) {
inspect_node.CreateString("children", NodeChildrenToString(data),
inspector);
}
inspect_node.CreateInt("current_level", current_level, inspector);
for (int32_t flutter_child_id : semantic_node.data.childrenInTraversalOrder) {
const auto inspect_name = "node_" + std::to_string(flutter_child_id);
FillInspectTree(flutter_child_id, current_level + 1,
inspect_node.CreateChild(inspect_name), inspector);
}
inspector->emplace(std::move(inspect_node));
}
#endif // !FLUTTER_RELEASE
} // namespace flutter_runner