24static constexpr char kTreeDumpInspectRootName[] =
"semantic_tree_root";
30 output +=
"kHasCheckedState|";
33 output +=
"kHasEnabledState|";
36 output +=
"kHasImplicitScrolling|";
39 output +=
"kHasToggledState|";
42 output +=
"kIsButton|";
45 output +=
"kIsChecked|";
48 output +=
"kIsEnabled|";
51 output +=
"kIsFocusable|";
54 output +=
"kIsFocused|";
57 output +=
"kIsHeader|";
60 output +=
"kIsHidden|";
63 output +=
"kIsImage|";
66 output +=
"kIsInMutuallyExclusiveGroup|";
69 output +=
"kIsKeyboardKey|";
75 output +=
"kIsLiveRegion|";
78 output +=
"kIsObscured|";
81 output +=
"kIsReadOnly|";
84 output +=
"kIsSelected|";
87 output +=
"kIsSlider|";
90 output +=
"kIsTextField|";
93 output +=
"kIsToggled|";
96 output +=
"kNamesRoute|";
99 output +=
"kScopesRoute|";
113 output +=
"kCustomAction|";
119 output +=
"kDecrease|";
122 output +=
"kDidGainAccessibilityFocus|";
125 output +=
"kDidLoseAccessibilityFocus|";
128 output +=
"kDismiss|";
131 output +=
"kIncrease|";
134 output +=
"kLongPress|";
138 output +=
"kMoveCursorBackwardByCharacter|";
141 output +=
"kMoveCursorBackwardByWord|";
144 output +=
"kMoveCursorForwardByCharacter|";
147 output +=
"kMoveCursorForwardByWord|";
153 output +=
"kScrollDown|";
156 output +=
"kScrollLeft|";
159 output +=
"kScrollRight|";
162 output +=
"kScrollUp|";
165 output +=
"kScrollToOffset|";
168 output +=
"kSetSelection|";
171 output +=
"kSetText|";
174 output +=
"kShowOnScreen|";
183 output +=
"kExpand|";
186 output +=
"kCollapse|";
194std::string NodeLocationToString(
const SkRect& rect) {
195 auto min_x = rect.fLeft;
196 auto min_y = rect.fTop;
197 auto max_x = rect.fRight;
198 auto max_y = rect.fBottom;
199 std::string location =
200 "min(" + std::to_string(min_x) +
", " + std::to_string(min_y) +
") max(" +
201 std::to_string(max_x) +
", " + std::to_string(max_y) +
")";
208 std::stringstream output;
210 output <<
"children in traversal order:[";
212 output << child_id <<
", ";
217 output <<
"children in hit test order:[";
219 output << child_id <<
", ";
233 fuchsia::accessibility::semantics::SemanticsManagerHandle semantics_manager,
234 fuchsia::ui::views::ViewRef view_ref,
235 inspect::Node inspect_node)
236 : set_semantics_enabled_callback_(
237 std::move(set_semantics_enabled_callback)),
238 dispatch_semantics_action_callback_(
239 std::move(dispatch_semantics_action_callback)),
241 fuchsia_semantics_manager_(semantics_manager.Bind()),
242 atomic_updates_(
std::make_shared<
std::
queue<FuchsiaAtomicUpdate>>()),
243 inspect_node_(
std::move(inspect_node)) {
244 fuchsia_semantics_manager_.set_error_handler([](zx_status_t status) {
245 FML_LOG(ERROR) <<
"Flutter cannot connect to SemanticsManager with status: "
246 << zx_status_get_string(status) <<
".";
248 fuchsia_semantics_manager_->RegisterViewForSemantics(
249 std::move(view_ref), binding_.NewBinding(), tree_ptr_.NewRequest());
255 inspect_node_tree_dump_ =
256 inspect_node_.CreateLazyValues(
"dump_fail", [
this]() {
257 inspect::Inspector inspector;
258 if (
auto it = nodes_.find(kRootNodeId); it == nodes_.end()) {
259 inspector.GetRoot().CreateString(
260 "empty_tree",
"this semantic tree is empty", &inspector);
264 inspector.GetRoot().CreateChild(kTreeDumpInspectRootName),
267 return fpromise::make_ok_promise(std::move(inspector));
273 return semantics_enabled_;
277 semantics_enabled_ = enabled;
283fuchsia::ui::gfx::BoundingBox AccessibilityBridge::GetNodeLocation(
285 fuchsia::ui::gfx::BoundingBox box;
286 box.min.x = node.
rect.fLeft;
287 box.min.y = node.
rect.fTop;
288 box.max.x = node.
rect.fRight;
289 box.max.y = node.
rect.fBottom;
293fuchsia::ui::gfx::mat4 AccessibilityBridge::GetNodeTransform(
295 return ConvertSkiaTransformToMat4(node.
transform);
298fuchsia::ui::gfx::mat4 AccessibilityBridge::ConvertSkiaTransformToMat4(
300 fuchsia::ui::gfx::mat4
value;
301 float* m =
value.matrix.data();
306fuchsia::accessibility::semantics::Attributes
308 size_t* added_size)
const {
309 fuchsia::accessibility::semantics::Attributes attributes;
311 if (node.
label.size() > fuchsia::accessibility::semantics::MAX_LABEL_SIZE) {
312 attributes.set_label(node.
label.substr(
313 0, fuchsia::accessibility::semantics::MAX_LABEL_SIZE));
314 *added_size += fuchsia::accessibility::semantics::MAX_LABEL_SIZE;
316 attributes.set_label(node.
label);
317 *added_size += node.
label.size();
320 if (node.
tooltip.size() > fuchsia::accessibility::semantics::MAX_LABEL_SIZE) {
321 attributes.set_secondary_label(node.
tooltip.substr(
322 0, fuchsia::accessibility::semantics::MAX_LABEL_SIZE));
323 *added_size += fuchsia::accessibility::semantics::MAX_LABEL_SIZE;
325 attributes.set_secondary_label(node.
tooltip);
326 *added_size += node.
tooltip.size();
330 attributes.set_is_keyboard_key(
true);
336fuchsia::accessibility::semantics::States AccessibilityBridge::GetNodeStates(
338 size_t* additional_size)
const {
339 fuchsia::accessibility::semantics::States states;
340 (*additional_size) +=
sizeof(fuchsia::accessibility::semantics::States);
344 states.set_checked_state(
345 fuchsia::accessibility::semantics::CheckedState::NONE);
347 states.set_checked_state(
349 ? fuchsia::accessibility::semantics::CheckedState::CHECKED
355 states.set_enabled_state(
357 ? fuchsia::accessibility::semantics::EnabledState::ENABLED
358 :
fuchsia::accessibility::semantics::EnabledState::DISABLED);
369 if (node.
value.size() > fuchsia::accessibility::semantics::MAX_VALUE_SIZE) {
370 states.set_value(node.
value.substr(
371 0, fuchsia::accessibility::semantics::MAX_VALUE_SIZE));
372 (*additional_size) += fuchsia::accessibility::semantics::MAX_VALUE_SIZE;
374 states.set_value(node.
value);
375 (*additional_size) += node.
value.size();
380 states.set_toggled_state(
382 ? fuchsia::accessibility::semantics::ToggledState::ON
383 :
fuchsia::accessibility::semantics::ToggledState::OFF);
389std::vector<fuchsia::accessibility::semantics::Action>
391 size_t* additional_size)
const {
392 std::vector<fuchsia::accessibility::semantics::Action> node_actions;
395 node_actions.push_back(fuchsia::accessibility::semantics::Action::DEFAULT);
398 node_actions.push_back(
399 fuchsia::accessibility::semantics::Action::SECONDARY);
402 node_actions.push_back(
403 fuchsia::accessibility::semantics::Action::SHOW_ON_SCREEN);
406 node_actions.push_back(
407 fuchsia::accessibility::semantics::Action::INCREMENT);
410 node_actions.push_back(
411 fuchsia::accessibility::semantics::Action::DECREMENT);
415 node_actions.size() *
sizeof(fuchsia::accessibility::semantics::Action);
419fuchsia::accessibility::semantics::Role AccessibilityBridge::GetNodeRole(
422 return fuchsia::accessibility::semantics::Role::BUTTON;
426 return fuchsia::accessibility::semantics::Role::TEXT_FIELD;
430 return fuchsia::accessibility::semantics::Role::LINK;
434 return fuchsia::accessibility::semantics::Role::SLIDER;
438 return fuchsia::accessibility::semantics::Role::HEADER;
441 return fuchsia::accessibility::semantics::Role::IMAGE;
450 return fuchsia::accessibility::semantics::Role::SLIDER;
458 return fuchsia::accessibility::semantics::Role::RADIO_BUTTON;
460 return fuchsia::accessibility::semantics::Role::CHECK_BOX;
465 return fuchsia::accessibility::semantics::Role::TOGGLE_SWITCH;
467 return fuchsia::accessibility::semantics::Role::UNKNOWN;
470std::unordered_set<int32_t> AccessibilityBridge::GetDescendants(
471 int32_t node_id)
const {
472 std::unordered_set<int32_t> descendents;
473 std::deque<int32_t> to_process = {node_id};
474 while (!to_process.empty()) {
475 int32_t
id = to_process.front();
476 to_process.pop_front();
477 descendents.emplace(
id);
479 auto it = nodes_.find(
id);
480 if (it != nodes_.end()) {
481 const auto& node = it->second.data;
482 for (
const auto& child : node.childrenInHitTestOrder) {
483 if (descendents.find(child) == descendents.end()) {
484 to_process.push_back(child);
491 FML_LOG(ERROR) <<
"Semantics Node " << child
492 <<
" has already been listed as a child of another "
493 "node, ignoring for parent "
507 <<
"Unexpectedly received a negative semantics node ID.";
508 return static_cast<uint32_t
>(flutter_node_id);
511void AccessibilityBridge::PruneUnreachableNodes(
512 FuchsiaAtomicUpdate* atomic_update) {
513 const auto& reachable_nodes = GetDescendants(kRootNodeId);
514 auto iter = nodes_.begin();
515 while (iter != nodes_.end()) {
516 int32_t
id = iter->first;
517 if (reachable_nodes.find(
id) == reachable_nodes.end()) {
519 iter = nodes_.erase(iter);
529 FML_LOG(ERROR) <<
"Semantics node with ID " << node_id
530 <<
" exceeded the maximum FIDL message size and may not "
531 "be delivered to the accessibility manager service.";
536 float view_pixel_ratio) {
537 if (update.empty()) {
540 FML_DCHECK(nodes_.find(kRootNodeId) != nodes_.end() ||
541 update.find(kRootNodeId) != update.end())
542 <<
"AccessibilityBridge received an update with out ever getting a root "
545 FuchsiaAtomicUpdate atomic_update;
546 bool has_root_node_update =
false;
552 for (
const auto& [flutter_node_id, flutter_node] : update) {
553 size_t this_node_size =
sizeof(fuchsia::accessibility::semantics::Node);
560 if (flutter_node.id == kRootNodeId) {
561 root_flutter_semantics_node_ = flutter_node;
562 has_root_node_update =
true;
566 nodes_[flutter_node.id].data = flutter_node;
568 fuchsia::accessibility::semantics::Node fuchsia_node;
569 std::vector<uint32_t> child_ids;
572 for (int32_t flutter_child_id : flutter_node.childrenInTraversalOrder) {
577 fuchsia_node.set_node_id(flutter_node.id)
578 .set_role(GetNodeRole(flutter_node))
579 .set_location(GetNodeLocation(flutter_node))
580 .set_transform(GetNodeTransform(flutter_node))
581 .set_attributes(GetNodeAttributes(flutter_node, &this_node_size))
582 .set_states(GetNodeStates(flutter_node, &this_node_size))
583 .set_actions(GetNodeActions(flutter_node, &this_node_size))
584 .set_child_ids(child_ids);
586 kNodeIdSize * flutter_node.childrenInTraversalOrder.size();
588 atomic_update.AddNodeUpdate(std::move(fuchsia_node), this_node_size);
592 if (has_root_node_update || last_seen_view_pixel_ratio_ != view_pixel_ratio) {
593 last_seen_view_pixel_ratio_ = view_pixel_ratio;
594 size_t root_node_size;
595 fuchsia::accessibility::semantics::Node root_update =
596 GetRootNodeUpdate(root_node_size);
597 atomic_update.AddNodeUpdate(std::move(root_update), root_node_size);
600 PruneUnreachableNodes(&atomic_update);
603 atomic_updates_->push(std::move(atomic_update));
604 if (atomic_updates_->size() == 1) {
606 Apply(&atomic_updates_->front());
610fuchsia::accessibility::semantics::Node AccessibilityBridge::GetRootNodeUpdate(
612 fuchsia::accessibility::semantics::Node root_fuchsia_node;
613 std::vector<uint32_t> child_ids;
614 node_size =
sizeof(fuchsia::accessibility::semantics::Node);
615 for (int32_t flutter_child_id :
616 root_flutter_semantics_node_.childrenInTraversalOrder) {
620 float inverse_view_pixel_ratio = 1.f / last_seen_view_pixel_ratio_;
621 SkM44 inverse_view_pixel_ratio_transform;
622 inverse_view_pixel_ratio_transform.setScale(inverse_view_pixel_ratio,
623 inverse_view_pixel_ratio, 1.f);
625 SkM44 result = root_flutter_semantics_node_.
transform *
626 inverse_view_pixel_ratio_transform;
627 nodes_[root_flutter_semantics_node_.
id].data = root_flutter_semantics_node_;
631 root_fuchsia_node.set_node_id(root_flutter_semantics_node_.
id)
632 .set_role(GetNodeRole(root_flutter_semantics_node_))
633 .set_location(GetNodeLocation(root_flutter_semantics_node_))
634 .set_transform(ConvertSkiaTransformToMat4(result))
636 GetNodeAttributes(root_flutter_semantics_node_, &node_size))
637 .set_states(GetNodeStates(root_flutter_semantics_node_, &node_size))
638 .set_actions(GetNodeActions(root_flutter_semantics_node_, &node_size))
639 .set_child_ids(child_ids);
642 return root_fuchsia_node;
646 fuchsia::accessibility::semantics::SemanticEvent semantic_event;
647 fuchsia::accessibility::semantics::AnnounceEvent announce_event;
648 announce_event.set_message(
message);
649 semantic_event.set_announce(std::move(announce_event));
651 tree_ptr_->SendSemanticEvent(std::move(semantic_event), []() {});
654void AccessibilityBridge::UpdateScreenRects() {
655 std::unordered_set<int32_t> visited_nodes;
662 float inverse_view_pixel_ratio = 1.f / last_seen_view_pixel_ratio_;
663 SkM44 inverse_view_pixel_ratio_transform;
664 inverse_view_pixel_ratio_transform.setScale(inverse_view_pixel_ratio,
665 inverse_view_pixel_ratio, 1.f);
667 UpdateScreenRects(kRootNodeId, inverse_view_pixel_ratio_transform,
671void AccessibilityBridge::UpdateScreenRects(
673 SkM44 parent_transform,
674 std::unordered_set<int32_t>* visited_nodes) {
675 auto it = nodes_.find(node_id);
676 if (it == nodes_.end()) {
677 FML_LOG(ERROR) <<
"UpdateScreenRects called on unknown node";
680 auto& node = it->second;
681 const auto& current_transform = parent_transform * node.data.
transform;
683 const auto& rect = node.data.
rect;
685 current_transform.map(rect.left(), rect.top(), 0, 1),
686 current_transform.map(rect.right(), rect.bottom(), 0, 1),
688 node.screen_rect.setLTRB(dst[0].
x, dst[0].
y, dst[1].
x, dst[1].
y);
689 node.screen_rect.sort();
691 visited_nodes->emplace(node_id);
693 for (uint32_t child_id : node.
data.childrenInHitTestOrder) {
694 if (visited_nodes->find(child_id) == visited_nodes->end()) {
695 UpdateScreenRects(child_id, current_transform, visited_nodes);
700std::optional<flutter::SemanticsAction>
701AccessibilityBridge::GetFlutterSemanticsAction(
702 fuchsia::accessibility::semantics::Action fuchsia_action,
704 switch (fuchsia_action) {
706 case fuchsia::accessibility::semantics::Action::DEFAULT:
710 case fuchsia::accessibility::semantics::Action::SECONDARY:
713 case fuchsia::accessibility::semantics::Action::SET_FOCUS:
715 <<
"Unsupported action SET_FOCUS sent for accessibility node "
719 case fuchsia::accessibility::semantics::Action::SET_VALUE:
721 <<
"Unsupported action SET_VALUE sent for accessibility node "
725 case fuchsia::accessibility::semantics::Action::SHOW_ON_SCREEN:
727 case fuchsia::accessibility::semantics::Action::INCREMENT:
729 case fuchsia::accessibility::semantics::Action::DECREMENT:
732 FML_LOG(WARNING) <<
"Unexpected action "
733 <<
static_cast<int32_t
>(fuchsia_action)
734 <<
" sent for accessibility node " << node_id;
742 fuchsia::accessibility::semantics::Action
action,
743 fuchsia::accessibility::semantics::SemanticListener::
744 OnAccessibilityActionRequestedCallback
callback) {
747 if (nodes_.find(node_id) == nodes_.end()) {
748 FML_LOG(ERROR) <<
"Attempted to send accessibility action "
749 <<
static_cast<int32_t
>(
action)
750 <<
" to unknown node id: " << node_id;
755 std::optional<flutter::SemanticsAction> flutter_action =
756 GetFlutterSemanticsAction(
action, node_id);
757 if (!flutter_action.has_value()) {
761 dispatch_semantics_action_callback_(
static_cast<int32_t
>(node_id),
762 flutter_action.value());
768 fuchsia::math::PointF local_point,
769 fuchsia::accessibility::semantics::SemanticListener::HitTestCallback
771 auto hit_node_id = GetHitNode(kRootNodeId, local_point.x, local_point.y);
773 fuchsia::accessibility::semantics::Hit hit;
776 hit.set_node_id(hit_node_id.value_or(kRootNodeId));
780std::optional<int32_t> AccessibilityBridge::GetHitNode(int32_t node_id,
783 auto it = nodes_.find(node_id);
784 if (it == nodes_.end()) {
785 FML_LOG(ERROR) <<
"Attempted to hit test unknown node id: " << node_id;
788 auto const& node = it->second;
792 for (int32_t child_id : node.
data.childrenInHitTestOrder) {
793 auto candidate = GetHitNode(child_id,
x,
y);
799 if (IsFocusable(node.data)) {
806bool AccessibilityBridge::IsFocusable(
827 return !node.
label.empty() || !node.
value.empty() || !node.
hint.empty();
831void AccessibilityBridge::OnSemanticsModeChanged(
833 OnSemanticsModeChangedCallback
callback) {
834 set_semantics_enabled_callback_(enabled);
838void AccessibilityBridge::FillInspectTree(int32_t flutter_node_id,
839 int32_t current_level,
840 inspect::Node inspect_node,
841 inspect::Inspector* inspector)
const {
842 const auto it = nodes_.find(flutter_node_id);
843 if (it == nodes_.end()) {
844 inspect_node.CreateString(
846 "This node has a parent in the semantic tree but has no value",
848 inspector->emplace(std::move(inspect_node));
851 const auto& semantic_node = it->second;
852 const auto&
data = semantic_node.data;
854 inspect_node.CreateInt(
"id",
data.id, inspector);
858 inspect_node.CreateString(
"label",
data.label, inspector);
859 if (!
data.hint.empty()) {
860 inspect_node.CreateString(
"hint",
data.hint, inspector);
862 if (!
data.value.empty()) {
863 inspect_node.CreateString(
"value",
data.value, inspector);
865 if (!
data.increasedValue.empty()) {
866 inspect_node.CreateString(
"increased_value",
data.increasedValue,
869 if (!
data.decreasedValue.empty()) {
870 inspect_node.CreateString(
"decreased_value",
data.decreasedValue,
874 if (
data.textDirection) {
875 inspect_node.CreateString(
876 "text_direction",
data.textDirection == 1 ?
"RTL" :
"LTR", inspector);
879 std::string flags = NodeFlagsToString(
data);
880 if (!flags.empty()) {
881 inspect_node.CreateString(
"flags", NodeFlagsToString(
data), inspector);
884 inspect_node.CreateString(
"actions", NodeActionsToString(
data), inspector);
887 inspect_node.CreateString(
888 "location", NodeLocationToString(semantic_node.screen_rect), inspector);
889 if (!
data.childrenInTraversalOrder.empty() ||
890 !
data.childrenInHitTestOrder.empty()) {
891 inspect_node.CreateString(
"children", NodeChildrenToString(
data),
895 inspect_node.CreateInt(
"current_level", current_level, inspector);
897 for (int32_t flutter_child_id : semantic_node.
data.childrenInTraversalOrder) {
898 const auto inspect_name =
"node_" + std::to_string(flutter_child_id);
899 FillInspectTree(flutter_child_id, current_level + 1,
900 inspect_node.CreateChild(inspect_name), inspector);
903 inspector->emplace(std::move(inspect_node));
907void AccessibilityBridge::Apply(FuchsiaAtomicUpdate* atomic_update) {
909 auto it = atomic_update->deletions.begin();
912 while (it != atomic_update->deletions.end()) {
913 std::vector<uint32_t> to_delete;
914 size_t end = std::min(atomic_update->deletions.size() - begin,
916 std::copy(std::make_move_iterator(it), std::make_move_iterator(it +
end),
917 std::back_inserter(to_delete));
918 tree_ptr_->DeleteSemanticNodes(std::move(to_delete));
923 std::vector<fuchsia::accessibility::semantics::Node> to_update;
924 size_t current_size = 0;
925 for (
auto& node_and_size : atomic_update->updates) {
927 tree_ptr_->UpdateSemanticNodes(std::move(to_update));
931 current_size += node_and_size.second;
932 to_update.push_back(std::move(node_and_size.first));
934 if (!to_update.empty()) {
935 tree_ptr_->UpdateSemanticNodes(std::move(to_update));
940 tree_ptr_->CommitUpdates(
941 [
this, atomic_updates = std::weak_ptr<std::queue<FuchsiaAtomicUpdate>>(
942 atomic_updates_)]() {
943 auto atomic_updates_ptr = atomic_updates.lock();
944 if (!atomic_updates_ptr) {
950 atomic_updates_ptr->pop();
951 if (!atomic_updates_ptr->empty()) {
952 Apply(&atomic_updates_ptr->front());
956 atomic_update->deletions.clear();
957 atomic_update->updates.clear();
960void AccessibilityBridge::FuchsiaAtomicUpdate::AddNodeUpdate(
961 fuchsia::accessibility::semantics::Node node,
963 if (size > kMaxMessageSize) {
970 updates.emplace_back(std::move(node), size);
973void AccessibilityBridge::FuchsiaAtomicUpdate::AddNodeDeletion(uint32_t
id) {
974 deletions.push_back(
id);