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 +=
"kSetSelection|";
168 output +=
"kSetText|";
171 output +=
"kShowOnScreen|";
182std::string NodeLocationToString(
const SkRect& rect) {
183 auto min_x = rect.fLeft;
184 auto min_y = rect.fTop;
185 auto max_x = rect.fRight;
186 auto max_y = rect.fBottom;
187 std::string location =
188 "min(" + std::to_string(min_x) +
", " + std::to_string(min_y) +
") max(" +
189 std::to_string(max_x) +
", " + std::to_string(max_y) +
")";
196 std::stringstream output;
198 output <<
"children in traversal order:[";
200 output << child_id <<
", ";
205 output <<
"children in hit test order:[";
207 output << child_id <<
", ";
221 fuchsia::accessibility::semantics::SemanticsManagerHandle semantics_manager,
222 fuchsia::ui::views::ViewRef view_ref,
223 inspect::Node inspect_node)
224 : set_semantics_enabled_callback_(
225 std::move(set_semantics_enabled_callback)),
226 dispatch_semantics_action_callback_(
227 std::move(dispatch_semantics_action_callback)),
229 fuchsia_semantics_manager_(semantics_manager.Bind()),
230 atomic_updates_(
std::make_shared<
std::
queue<FuchsiaAtomicUpdate>>()),
231 inspect_node_(
std::move(inspect_node)) {
232 fuchsia_semantics_manager_.set_error_handler([](zx_status_t status) {
233 FML_LOG(
ERROR) <<
"Flutter cannot connect to SemanticsManager with status: "
234 << zx_status_get_string(status) <<
".";
236 fuchsia_semantics_manager_->RegisterViewForSemantics(
237 std::move(view_ref), binding_.NewBinding(), tree_ptr_.NewRequest());
243 inspect_node_tree_dump_ =
244 inspect_node_.CreateLazyValues(
"dump_fail", [
this]() {
245 inspect::Inspector inspector;
246 if (
auto it = nodes_.find(kRootNodeId); it == nodes_.end()) {
247 inspector.GetRoot().CreateString(
248 "empty_tree",
"this semantic tree is empty", &inspector);
252 inspector.GetRoot().CreateChild(kTreeDumpInspectRootName),
255 return fpromise::make_ok_promise(std::move(inspector));
261 return semantics_enabled_;
265 semantics_enabled_ = enabled;
271fuchsia::ui::gfx::BoundingBox AccessibilityBridge::GetNodeLocation(
273 fuchsia::ui::gfx::BoundingBox box;
276 box.min.z =
static_cast<float>(node.
elevation);
279 box.max.z =
static_cast<float>(node.
thickness);
283fuchsia::ui::gfx::mat4 AccessibilityBridge::GetNodeTransform(
285 return ConvertSkiaTransformToMat4(node.
transform);
288fuchsia::ui::gfx::mat4 AccessibilityBridge::ConvertSkiaTransformToMat4(
290 fuchsia::ui::gfx::mat4
value;
291 float*
m =
value.matrix.data();
296fuchsia::accessibility::semantics::Attributes
298 size_t* added_size)
const {
299 fuchsia::accessibility::semantics::Attributes attributes;
301 if (node.
label.size() > fuchsia::accessibility::semantics::MAX_LABEL_SIZE) {
302 attributes.set_label(node.
label.substr(
303 0, fuchsia::accessibility::semantics::MAX_LABEL_SIZE));
304 *added_size += fuchsia::accessibility::semantics::MAX_LABEL_SIZE;
306 attributes.set_label(node.
label);
307 *added_size += node.
label.size();
310 if (node.
tooltip.size() > fuchsia::accessibility::semantics::MAX_LABEL_SIZE) {
311 attributes.set_secondary_label(node.
tooltip.substr(
312 0, fuchsia::accessibility::semantics::MAX_LABEL_SIZE));
313 *added_size += fuchsia::accessibility::semantics::MAX_LABEL_SIZE;
315 attributes.set_secondary_label(node.
tooltip);
316 *added_size += node.
tooltip.size();
320 attributes.set_is_keyboard_key(
true);
326fuchsia::accessibility::semantics::States AccessibilityBridge::GetNodeStates(
328 size_t* additional_size)
const {
329 fuchsia::accessibility::semantics::States states;
330 (*additional_size) +=
sizeof(fuchsia::accessibility::semantics::States);
334 states.set_checked_state(
335 fuchsia::accessibility::semantics::CheckedState::NONE);
337 states.set_checked_state(
339 ? fuchsia::accessibility::semantics::CheckedState::CHECKED
340 : fuchsia::accessibility::semantics::CheckedState::UNCHECKED);
345 states.set_enabled_state(
347 ? fuchsia::accessibility::semantics::EnabledState::ENABLED
348 : fuchsia::accessibility::semantics::EnabledState::DISABLED);
358 if (node.
value.size() > fuchsia::accessibility::semantics::MAX_VALUE_SIZE) {
359 states.set_value(node.
value.substr(
360 0, fuchsia::accessibility::semantics::MAX_VALUE_SIZE));
361 (*additional_size) += fuchsia::accessibility::semantics::MAX_VALUE_SIZE;
363 states.set_value(node.
value);
364 (*additional_size) += node.
value.size();
369 states.set_toggled_state(
371 ? fuchsia::accessibility::semantics::ToggledState::ON
372 : fuchsia::accessibility::semantics::ToggledState::OFF);
378std::vector<fuchsia::accessibility::semantics::Action>
380 size_t* additional_size)
const {
381 std::vector<fuchsia::accessibility::semantics::Action> node_actions;
384 node_actions.push_back(fuchsia::accessibility::semantics::Action::DEFAULT);
387 node_actions.push_back(
388 fuchsia::accessibility::semantics::Action::SECONDARY);
391 node_actions.push_back(
392 fuchsia::accessibility::semantics::Action::SHOW_ON_SCREEN);
395 node_actions.push_back(
396 fuchsia::accessibility::semantics::Action::INCREMENT);
399 node_actions.push_back(
400 fuchsia::accessibility::semantics::Action::DECREMENT);
404 node_actions.size() *
sizeof(fuchsia::accessibility::semantics::Action);
408fuchsia::accessibility::semantics::Role AccessibilityBridge::GetNodeRole(
411 return fuchsia::accessibility::semantics::Role::BUTTON;
415 return fuchsia::accessibility::semantics::Role::TEXT_FIELD;
419 return fuchsia::accessibility::semantics::Role::LINK;
423 return fuchsia::accessibility::semantics::Role::SLIDER;
427 return fuchsia::accessibility::semantics::Role::HEADER;
430 return fuchsia::accessibility::semantics::Role::IMAGE;
439 return fuchsia::accessibility::semantics::Role::SLIDER;
447 return fuchsia::accessibility::semantics::Role::RADIO_BUTTON;
449 return fuchsia::accessibility::semantics::Role::CHECK_BOX;
454 return fuchsia::accessibility::semantics::Role::TOGGLE_SWITCH;
456 return fuchsia::accessibility::semantics::Role::UNKNOWN;
459std::unordered_set<int32_t> AccessibilityBridge::GetDescendants(
460 int32_t node_id)
const {
461 std::unordered_set<int32_t> descendents;
462 std::deque<int32_t> to_process = {node_id};
463 while (!to_process.empty()) {
464 int32_t
id = to_process.front();
465 to_process.pop_front();
466 descendents.emplace(
id);
468 auto it = nodes_.find(
id);
469 if (it != nodes_.end()) {
470 const auto& node = it->second.data;
471 for (
const auto& child : node.childrenInHitTestOrder) {
472 if (descendents.find(child) == descendents.end()) {
473 to_process.push_back(child);
481 <<
" has already been listed as a child of another "
482 "node, ignoring for parent "
496 <<
"Unexpectedly received a negative semantics node ID.";
497 return static_cast<uint32_t
>(flutter_node_id);
500void AccessibilityBridge::PruneUnreachableNodes(
501 FuchsiaAtomicUpdate* atomic_update) {
502 const auto& reachable_nodes = GetDescendants(kRootNodeId);
503 auto iter = nodes_.begin();
504 while (iter != nodes_.end()) {
505 int32_t
id = iter->first;
506 if (reachable_nodes.find(
id) == reachable_nodes.end()) {
508 iter = nodes_.erase(iter);
519 <<
" exceeded the maximum FIDL message size and may not "
520 "be delivered to the accessibility manager service.";
525 float view_pixel_ratio) {
529 FML_DCHECK(nodes_.find(kRootNodeId) != nodes_.end() ||
531 <<
"AccessibilityBridge received an update with out ever getting a root "
534 FuchsiaAtomicUpdate atomic_update;
535 bool has_root_node_update =
false;
541 for (
const auto& [flutter_node_id, flutter_node] :
update) {
542 size_t this_node_size =
sizeof(fuchsia::accessibility::semantics::Node);
549 if (flutter_node.id == kRootNodeId) {
550 root_flutter_semantics_node_ = flutter_node;
551 has_root_node_update =
true;
555 nodes_[flutter_node.id].data = flutter_node;
557 fuchsia::accessibility::semantics::Node fuchsia_node;
558 std::vector<uint32_t> child_ids;
561 for (int32_t flutter_child_id : flutter_node.childrenInTraversalOrder) {
566 fuchsia_node.set_node_id(flutter_node.id)
567 .set_role(GetNodeRole(flutter_node))
568 .set_location(GetNodeLocation(flutter_node))
569 .set_transform(GetNodeTransform(flutter_node))
570 .set_attributes(GetNodeAttributes(flutter_node, &this_node_size))
571 .set_states(GetNodeStates(flutter_node, &this_node_size))
572 .set_actions(GetNodeActions(flutter_node, &this_node_size))
573 .set_child_ids(child_ids);
575 kNodeIdSize * flutter_node.childrenInTraversalOrder.size();
577 atomic_update.AddNodeUpdate(std::move(fuchsia_node), this_node_size);
581 if (has_root_node_update || last_seen_view_pixel_ratio_ != view_pixel_ratio) {
582 last_seen_view_pixel_ratio_ = view_pixel_ratio;
583 size_t root_node_size;
584 fuchsia::accessibility::semantics::Node root_update =
585 GetRootNodeUpdate(root_node_size);
586 atomic_update.AddNodeUpdate(std::move(root_update), root_node_size);
589 PruneUnreachableNodes(&atomic_update);
592 atomic_updates_->push(std::move(atomic_update));
593 if (atomic_updates_->size() == 1) {
595 Apply(&atomic_updates_->front());
599fuchsia::accessibility::semantics::Node AccessibilityBridge::GetRootNodeUpdate(
601 fuchsia::accessibility::semantics::Node root_fuchsia_node;
602 std::vector<uint32_t> child_ids;
603 node_size =
sizeof(fuchsia::accessibility::semantics::Node);
604 for (int32_t flutter_child_id :
605 root_flutter_semantics_node_.childrenInTraversalOrder) {
609 float inverse_view_pixel_ratio = 1.f / last_seen_view_pixel_ratio_;
610 SkM44 inverse_view_pixel_ratio_transform;
611 inverse_view_pixel_ratio_transform.
setScale(inverse_view_pixel_ratio,
612 inverse_view_pixel_ratio, 1.f);
615 inverse_view_pixel_ratio_transform;
616 nodes_[root_flutter_semantics_node_.
id].data = root_flutter_semantics_node_;
620 root_fuchsia_node.set_node_id(root_flutter_semantics_node_.
id)
621 .set_role(GetNodeRole(root_flutter_semantics_node_))
622 .set_location(GetNodeLocation(root_flutter_semantics_node_))
623 .set_transform(ConvertSkiaTransformToMat4(
result))
625 GetNodeAttributes(root_flutter_semantics_node_, &node_size))
626 .set_states(GetNodeStates(root_flutter_semantics_node_, &node_size))
627 .set_actions(GetNodeActions(root_flutter_semantics_node_, &node_size))
628 .set_child_ids(child_ids);
631 return root_fuchsia_node;
635 fuchsia::accessibility::semantics::SemanticEvent semantic_event;
636 fuchsia::accessibility::semantics::AnnounceEvent announce_event;
637 announce_event.set_message(
message);
638 semantic_event.set_announce(std::move(announce_event));
640 tree_ptr_->SendSemanticEvent(std::move(semantic_event), []() {});
643void AccessibilityBridge::UpdateScreenRects() {
644 std::unordered_set<int32_t> visited_nodes;
651 float inverse_view_pixel_ratio = 1.f / last_seen_view_pixel_ratio_;
652 SkM44 inverse_view_pixel_ratio_transform;
653 inverse_view_pixel_ratio_transform.
setScale(inverse_view_pixel_ratio,
654 inverse_view_pixel_ratio, 1.f);
656 UpdateScreenRects(kRootNodeId, inverse_view_pixel_ratio_transform,
660void AccessibilityBridge::UpdateScreenRects(
662 SkM44 parent_transform,
663 std::unordered_set<int32_t>* visited_nodes) {
664 auto it = nodes_.find(node_id);
665 if (it == nodes_.end()) {
666 FML_LOG(
ERROR) <<
"UpdateScreenRects called on unknown node";
669 auto& node = it->second;
670 const auto& current_transform = parent_transform * node.data.
transform;
674 current_transform.map(
rect.left(),
rect.top(), 0, 1),
675 current_transform.map(
rect.right(),
rect.bottom(), 0, 1),
677 node.screen_rect.setLTRB(dst[0].
x, dst[0].
y, dst[1].
x, dst[1].
y);
678 node.screen_rect.sort();
680 visited_nodes->emplace(node_id);
682 for (uint32_t child_id : node.
data.childrenInHitTestOrder) {
683 if (visited_nodes->find(child_id) == visited_nodes->end()) {
684 UpdateScreenRects(child_id, current_transform, visited_nodes);
689std::optional<flutter::SemanticsAction>
690AccessibilityBridge::GetFlutterSemanticsAction(
691 fuchsia::accessibility::semantics::Action fuchsia_action,
693 switch (fuchsia_action) {
695 case fuchsia::accessibility::semantics::Action::DEFAULT:
699 case fuchsia::accessibility::semantics::Action::SECONDARY:
702 case fuchsia::accessibility::semantics::Action::SET_FOCUS:
704 <<
"Unsupported action SET_FOCUS sent for accessibility node "
708 case fuchsia::accessibility::semantics::Action::SET_VALUE:
710 <<
"Unsupported action SET_VALUE sent for accessibility node "
714 case fuchsia::accessibility::semantics::Action::SHOW_ON_SCREEN:
716 case fuchsia::accessibility::semantics::Action::INCREMENT:
718 case fuchsia::accessibility::semantics::Action::DECREMENT:
721 FML_LOG(WARNING) <<
"Unexpected action "
722 <<
static_cast<int32_t
>(fuchsia_action)
723 <<
" sent for accessibility node " << node_id;
731 fuchsia::accessibility::semantics::Action
action,
732 fuchsia::accessibility::semantics::SemanticListener::
733 OnAccessibilityActionRequestedCallback
callback) {
736 if (nodes_.find(node_id) == nodes_.end()) {
737 FML_LOG(
ERROR) <<
"Attempted to send accessibility action "
738 <<
static_cast<int32_t
>(
action)
739 <<
" to unknown node id: " << node_id;
744 std::optional<flutter::SemanticsAction> flutter_action =
745 GetFlutterSemanticsAction(
action, node_id);
746 if (!flutter_action.has_value()) {
750 dispatch_semantics_action_callback_(
static_cast<int32_t
>(node_id),
751 flutter_action.value());
757 fuchsia::math::PointF local_point,
758 fuchsia::accessibility::semantics::SemanticListener::HitTestCallback
760 auto hit_node_id = GetHitNode(kRootNodeId, local_point.x, local_point.y);
762 fuchsia::accessibility::semantics::Hit hit;
765 hit.set_node_id(hit_node_id.value_or(kRootNodeId));
769std::optional<int32_t> AccessibilityBridge::GetHitNode(int32_t node_id,
772 auto it = nodes_.find(node_id);
773 if (it == nodes_.end()) {
774 FML_LOG(
ERROR) <<
"Attempted to hit test unknown node id: " << node_id;
777 auto const& node = it->second;
778 if (node.data.
flags &
780 !node.screen_rect.contains(
x,
y)) {
783 for (int32_t child_id : node.
data.childrenInHitTestOrder) {
784 auto candidate = GetHitNode(child_id,
x,
y);
790 if (IsFocusable(node.data)) {
797bool AccessibilityBridge::IsFocusable(
818 return !node.
label.empty() || !node.
value.empty() || !node.
hint.empty();
824 OnSemanticsModeChangedCallback
callback) {
825 set_semantics_enabled_callback_(enabled);
829void AccessibilityBridge::FillInspectTree(int32_t flutter_node_id,
830 int32_t current_level,
831 inspect::Node inspect_node,
832 inspect::Inspector* inspector)
const {
833 const auto it = nodes_.find(flutter_node_id);
834 if (it == nodes_.end()) {
835 inspect_node.CreateString(
837 "This node has a parent in the semantic tree but has no value",
839 inspector->emplace(std::move(inspect_node));
842 const auto& semantic_node = it->second;
843 const auto& data = semantic_node.data;
845 inspect_node.CreateInt(
"id", data.id, inspector);
849 inspect_node.CreateString(
"label", data.label, inspector);
850 if (!data.hint.empty()) {
851 inspect_node.CreateString(
"hint", data.hint, inspector);
853 if (!
data.value.empty()) {
854 inspect_node.CreateString(
"value",
data.value, inspector);
856 if (!
data.increasedValue.empty()) {
857 inspect_node.CreateString(
"increased_value",
data.increasedValue,
860 if (!
data.decreasedValue.empty()) {
861 inspect_node.CreateString(
"decreased_value",
data.decreasedValue,
865 if (
data.textDirection) {
866 inspect_node.CreateString(
867 "text_direction",
data.textDirection == 1 ?
"RTL" :
"LTR", inspector);
871 inspect_node.CreateString(
"flags", NodeFlagsToString(data), inspector);
874 inspect_node.CreateString(
"actions", NodeActionsToString(data), inspector);
877 inspect_node.CreateString(
878 "location", NodeLocationToString(semantic_node.screen_rect), inspector);
879 if (!
data.childrenInTraversalOrder.empty() ||
880 !
data.childrenInHitTestOrder.empty()) {
881 inspect_node.CreateString(
"children", NodeChildrenToString(data),
885 inspect_node.CreateInt(
"current_level", current_level, inspector);
887 for (int32_t flutter_child_id : semantic_node.
data.childrenInTraversalOrder) {
888 const auto inspect_name =
"node_" + std::to_string(flutter_child_id);
889 FillInspectTree(flutter_child_id, current_level + 1,
890 inspect_node.CreateChild(inspect_name), inspector);
893 inspector->emplace(std::move(inspect_node));
897void AccessibilityBridge::Apply(FuchsiaAtomicUpdate* atomic_update) {
899 auto it = atomic_update->deletions.begin();
902 while (it != atomic_update->deletions.end()) {
903 std::vector<uint32_t> to_delete;
904 size_t end = std::min(atomic_update->deletions.size() -
begin,
906 std::copy(std::make_move_iterator(it), std::make_move_iterator(it +
end),
907 std::back_inserter(to_delete));
908 tree_ptr_->DeleteSemanticNodes(std::move(to_delete));
913 std::vector<fuchsia::accessibility::semantics::Node> to_update;
914 size_t current_size = 0;
915 for (
auto& node_and_size : atomic_update->updates) {
917 tree_ptr_->UpdateSemanticNodes(std::move(to_update));
921 current_size += node_and_size.second;
922 to_update.push_back(std::move(node_and_size.first));
924 if (!to_update.empty()) {
925 tree_ptr_->UpdateSemanticNodes(std::move(to_update));
930 tree_ptr_->CommitUpdates(
931 [
this, atomic_updates = std::weak_ptr<std::queue<FuchsiaAtomicUpdate>>(
932 atomic_updates_)]() {
933 auto atomic_updates_ptr = atomic_updates.lock();
934 if (!atomic_updates_ptr) {
940 atomic_updates_ptr->pop();
941 if (!atomic_updates_ptr->empty()) {
942 Apply(&atomic_updates_ptr->front());
946 atomic_update->deletions.clear();
947 atomic_update->updates.clear();
950void AccessibilityBridge::FuchsiaAtomicUpdate::AddNodeUpdate(
951 fuchsia::accessibility::semantics::Node node,
953 if (size > kMaxMessageSize) {
960 updates.emplace_back(std::move(node), size);
963void AccessibilityBridge::FuchsiaAtomicUpdate::AddNodeDeletion(uint32_t
id) {
964 deletions.push_back(
id);