chromium/ui/accessibility/platform/fuchsia/browser_accessibility_fuchsia.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/accessibility/platform/fuchsia/browser_accessibility_fuchsia.h"

#include "base/fuchsia/fuchsia_logging.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/platform/fuchsia/browser_accessibility_manager_fuchsia.h"
#include "ui/accessibility/platform/fuchsia/accessibility_bridge_fuchsia_registry.h"
#include "ui/gfx/geometry/rect_conversions.h"

namespace ui {

using AXRole = ax::mojom::Role;
using FuchsiaRole = fuchsia_accessibility_semantics::Role;

BrowserAccessibilityFuchsia::BrowserAccessibilityFuchsia(
    BrowserAccessibilityManager* manager,
    AXNode* node)
    : BrowserAccessibility(manager, node) {
  platform_node_ =
      static_cast<AXPlatformNodeFuchsia*>(AXPlatformNode::Create(this));
}

AccessibilityBridgeFuchsia*
BrowserAccessibilityFuchsia::GetAccessibilityBridge() const {
  BrowserAccessibilityManagerFuchsia* manager_fuchsia =
      static_cast<BrowserAccessibilityManagerFuchsia*>(manager());
  DCHECK(manager_fuchsia);

  return manager_fuchsia->GetAccessibilityBridge();
}

// static
std::unique_ptr<BrowserAccessibility> BrowserAccessibility::Create(
    BrowserAccessibilityManager* manager,
    AXNode* node) {
  return std::make_unique<BrowserAccessibilityFuchsia>(manager, node);
}

BrowserAccessibilityFuchsia::~BrowserAccessibilityFuchsia() {
  DeleteNode();
  platform_node_->Destroy();
}

uint32_t BrowserAccessibilityFuchsia::GetFuchsiaNodeID() const {
  return static_cast<uint32_t>(GetUniqueId());
}

fuchsia_accessibility_semantics::Node
BrowserAccessibilityFuchsia::ToFuchsiaNodeData() const {
  return {{
      .node_id = GetFuchsiaNodeID(),
      .role = GetFuchsiaRole(),
      .states = GetFuchsiaStates(),
      .attributes = GetFuchsiaAttributes(),
      .actions = GetFuchsiaActions(),
      .child_ids = GetFuchsiaChildIDs(),
      .location = GetFuchsiaLocation(),
      .container_id = GetOffsetContainerOrRootNodeID(),
      .node_to_container_transform = GetFuchsiaTransform(),
  }};
}

void BrowserAccessibilityFuchsia::OnDataChanged() {
  BrowserAccessibility::OnDataChanged();

  // Declare this node as the fuchsia tree root if it's the root of the main
  // frame's tree.
  if (manager()->IsRootFrameManager() &&
      manager()->GetBrowserAccessibilityRoot() == this) {
    AccessibilityBridgeFuchsia* accessibility_bridge = GetAccessibilityBridge();
    if (accessibility_bridge)
      accessibility_bridge->SetRootID(GetUniqueId());
  }

  UpdateNode();
}

void BrowserAccessibilityFuchsia::OnLocationChanged() {
  UpdateNode();
}

AXPlatformNode* BrowserAccessibilityFuchsia::GetAXPlatformNode() const {
  return platform_node_;
}

BrowserAccessibilityFuchsia* ToBrowserAccessibilityFuchsia(
    BrowserAccessibility* obj) {
  return static_cast<BrowserAccessibilityFuchsia*>(obj);
}

std::vector<uint32_t> BrowserAccessibilityFuchsia::GetFuchsiaChildIDs() const {
  std::vector<uint32_t> child_ids;

  // TODO(abrusher): Switch back to using platform children.
  for (const auto* child : AllChildren()) {
    const BrowserAccessibilityFuchsia* fuchsia_child =
        static_cast<const BrowserAccessibilityFuchsia*>(child);
    DCHECK(fuchsia_child);

    child_ids.push_back(fuchsia_child->GetFuchsiaNodeID());
  }

  return child_ids;
}

bool BrowserAccessibilityFuchsia::IsFuchsiaDefaultAction() const {
  // Nodes given the Default action map to Fuchsia's Default action.
  if (HasAction(ax::mojom::Action::kDoDefault)) {
    return true;
  }
  // Action verbs other than None and ClickAncestor also map to the Fuchsia's
  // Default action. ClickAncestor should be excluded, as the node itself is not
  // clickable (only an ancestor in the tree).
  auto verb = GetData().GetDefaultActionVerb();
  return verb != ax::mojom::DefaultActionVerb::kNone &&
         verb != ax::mojom::DefaultActionVerb::kClickAncestor;
}

std::vector<fuchsia_accessibility_semantics::Action>
BrowserAccessibilityFuchsia::GetFuchsiaActions() const {
  std::vector<fuchsia_accessibility_semantics::Action> actions;

  if (IsFuchsiaDefaultAction()) {
    actions.push_back(fuchsia_accessibility_semantics::Action::kDefault);
  }

  if (HasAction(ax::mojom::Action::kFocus))
    actions.push_back(fuchsia_accessibility_semantics::Action::kSetFocus);

  if (HasAction(ax::mojom::Action::kSetValue))
    actions.push_back(fuchsia_accessibility_semantics::Action::kSetValue);

  if (HasAction(ax::mojom::Action::kScrollToMakeVisible)) {
    actions.push_back(fuchsia_accessibility_semantics::Action::kShowOnScreen);
  }

  return actions;
}

fuchsia_accessibility_semantics::Role
BrowserAccessibilityFuchsia::GetFuchsiaRole() const {
  auto role = GetRole();

  switch (role) {
    case AXRole::kButton:
      return FuchsiaRole::kButton;
    case AXRole::kCell:
      return FuchsiaRole::kCell;
    case AXRole::kCheckBox:
      return FuchsiaRole::kCheckBox;
    case AXRole::kColumnHeader:
      return FuchsiaRole::kColumnHeader;
    case AXRole::kGrid:
      return FuchsiaRole::kGrid;
    case AXRole::kGridCell:
      return FuchsiaRole::kCell;
    case AXRole::kHeader:
      return FuchsiaRole::kHeader;
    case AXRole::kImage:
      return FuchsiaRole::kImage;
    case AXRole::kLink:
      return FuchsiaRole::kLink;
    case AXRole::kList:
      return FuchsiaRole::kList;
    case AXRole::kListItem:
      return FuchsiaRole::kListElement;
    case AXRole::kListMarker:
      return FuchsiaRole::kListElementMarker;
    case AXRole::kParagraph:
      return FuchsiaRole::kParagraph;
    case AXRole::kRadioButton:
      return FuchsiaRole::kRadioButton;
    case AXRole::kRowGroup:
      return FuchsiaRole::kRowGroup;
    case AXRole::kSearchBox:
      return FuchsiaRole::kSearchBox;
    case AXRole::kSlider:
      return FuchsiaRole::kSlider;
    case AXRole::kStaticText:
      return FuchsiaRole::kStaticText;
    case AXRole::kTable:
      return FuchsiaRole::kTable;
    case AXRole::kRow:
      return FuchsiaRole::kTableRow;
    case AXRole::kTextField:
      return FuchsiaRole::kTextField;
    case AXRole::kTextFieldWithComboBox:
      return FuchsiaRole::kTextFieldWithComboBox;
    default:
      return FuchsiaRole::kUnknown;
  }
}

fuchsia_accessibility_semantics::States
BrowserAccessibilityFuchsia::GetFuchsiaStates() const {
  fuchsia_accessibility_semantics::States states;

  // Convert checked state.
  if (HasIntAttribute(ax::mojom::IntAttribute::kCheckedState)) {
    ax::mojom::CheckedState ax_state = GetData().GetCheckedState();
    switch (ax_state) {
      case ax::mojom::CheckedState::kNone:
        states.checked_state(
            fuchsia_accessibility_semantics::CheckedState::kNone);
        break;
      case ax::mojom::CheckedState::kTrue:
        states.checked_state(
            fuchsia_accessibility_semantics::CheckedState::kChecked);
        break;
      case ax::mojom::CheckedState::kFalse:
        states.checked_state(
            fuchsia_accessibility_semantics::CheckedState::kUnchecked);
        break;
      case ax::mojom::CheckedState::kMixed:
        states.checked_state(
            fuchsia_accessibility_semantics::CheckedState::kMixed);
        break;
    }
  }

  // Convert selected state.
  // Indicates whether a node has been selected.
  if (GetData().IsSelectable() &&
      HasBoolAttribute(ax::mojom::BoolAttribute::kSelected)) {
    states.selected(GetBoolAttribute(ax::mojom::BoolAttribute::kSelected));
  }

  // Indicates if the node is hidden.
  states.hidden(IsInvisibleOrIgnored());

  // The user entered value of the node, if applicable.
  if (HasStringAttribute(ax::mojom::StringAttribute::kValue)) {
    const std::string& value =
        GetStringAttribute(ax::mojom::StringAttribute::kValue);
    states.value(
        value.substr(0, fuchsia_accessibility_semantics::kMaxLabelSize));
  }

  // The value a range element currently has.
  if (HasFloatAttribute(ax::mojom::FloatAttribute::kValueForRange)) {
    states.range_value(
        GetFloatAttribute(ax::mojom::FloatAttribute::kValueForRange));
  }

  // The scroll offsets, if the element is a scrollable container.
  const float x_scroll_offset =
      GetIntAttribute(ax::mojom::IntAttribute::kScrollX);
  const float y_scroll_offset =
      GetIntAttribute(ax::mojom::IntAttribute::kScrollY);
  if (x_scroll_offset || y_scroll_offset)
    states.viewport_offset({{x_scroll_offset, y_scroll_offset}});

  if (IsFocusable())
    states.focusable(true);

  states.has_input_focus(IsFocused());

  return states;
}

fuchsia_accessibility_semantics::Attributes
BrowserAccessibilityFuchsia::GetFuchsiaAttributes() const {
  fuchsia_accessibility_semantics::Attributes attributes;
  if (HasStringAttribute(ax::mojom::StringAttribute::kName)) {
    const std::string& name =
        GetStringAttribute(ax::mojom::StringAttribute::kName);
    attributes.label(
        name.substr(0, fuchsia_accessibility_semantics::kMaxLabelSize));
  }

  if (HasStringAttribute(ax::mojom::StringAttribute::kDescription)) {
    const std::string& description =
        GetStringAttribute(ax::mojom::StringAttribute::kDescription);
    attributes.secondary_label(
        description.substr(0, fuchsia_accessibility_semantics::kMaxLabelSize));
  }

  if (GetData().IsRangeValueSupported()) {
    fuchsia_accessibility_semantics::RangeAttributes range_attributes;
    if (HasFloatAttribute(ax::mojom::FloatAttribute::kMinValueForRange)) {
      range_attributes.min_value(
          GetFloatAttribute(ax::mojom::FloatAttribute::kMinValueForRange));
    }
    if (HasFloatAttribute(ax::mojom::FloatAttribute::kMaxValueForRange)) {
      range_attributes.max_value(
          GetFloatAttribute(ax::mojom::FloatAttribute::kMaxValueForRange));
    }
    if (HasFloatAttribute(ax::mojom::FloatAttribute::kStepValueForRange)) {
      range_attributes.step_delta(
          GetFloatAttribute(ax::mojom::FloatAttribute::kStepValueForRange));
    }
    attributes.range(std::move(range_attributes));
  }

  if (IsTable()) {
    fuchsia_accessibility_semantics::TableAttributes table_attributes;
    auto col_count = GetTableColCount();
    if (col_count)
      table_attributes.number_of_columns(*col_count);

    auto row_count = GetTableRowCount();
    if (row_count)
      table_attributes.number_of_rows(*row_count);

    if (!table_attributes.IsEmpty())
      attributes.table_attributes(std::move(table_attributes));
  }

  if (IsTableRow()) {
    fuchsia_accessibility_semantics::TableRowAttributes table_row_attributes;
    auto row_index = GetTableRowRowIndex();
    if (row_index) {
      table_row_attributes.row_index(*row_index);
      attributes.table_row_attributes(std::move(table_row_attributes));
    }
  }

  if (IsTableCellOrHeader()) {
    fuchsia_accessibility_semantics::TableCellAttributes table_cell_attributes;

    auto col_index = GetTableCellColIndex();
    if (col_index)
      table_cell_attributes.column_index(*col_index);

    auto row_index = GetTableCellRowIndex();
    if (row_index)
      table_cell_attributes.row_index(*row_index);

    auto col_span = GetTableCellColSpan();
    if (col_span)
      table_cell_attributes.column_span(*col_span);

    auto row_span = GetTableCellRowSpan();
    if (row_span)
      table_cell_attributes.row_span(*row_span);

    if (!table_cell_attributes.IsEmpty())
      attributes.table_cell_attributes(std::move(table_cell_attributes));
  }

  if (IsList()) {
    std::optional<int> size = GetSetSize();
    if (size) {
      fuchsia_accessibility_semantics::SetAttributes list_attributes;
      list_attributes.size(*size);
      attributes.list_attributes(std::move(list_attributes));
    }
  }

  if (IsListElement()) {
    std::optional<int> index = GetPosInSet();
    if (index) {
      fuchsia_accessibility_semantics::SetAttributes list_element_attributes;
      list_element_attributes.index(*index);
      attributes.list_element_attributes(std::move(list_element_attributes));
    }
  }

  return attributes;
}

fuchsia_ui_gfx::BoundingBox BrowserAccessibilityFuchsia::GetFuchsiaLocation()
    const {
  const gfx::RectF& bounds = GetLocation();

  return {{
      .min = {{
          .x = bounds.x(),
          .y = bounds.y(),
          .z = 0.0f,
      }},
      .max = {{
          .x = bounds.right(),
          .y = bounds.bottom(),
          .z = 0.0f,
      }},
  }};
}

fuchsia_ui_gfx::Mat4 BrowserAccessibilityFuchsia::GetFuchsiaTransform() const {
  // Get AXNode's explicit transform.
  gfx::Transform transform;
  if (GetData().relative_bounds.transform)
    transform = *GetData().relative_bounds.transform;

  // Convert to fuchsia's transform type.
  std::array<float, 16> mat = {};
  transform.GetColMajorF(mat.data());
  return {{.matrix = mat}};
}

uint32_t BrowserAccessibilityFuchsia::GetOffsetContainerOrRootNodeID() const {
  int offset_container_id = GetData().relative_bounds.offset_container_id;

  BrowserAccessibility* offset_container =
      offset_container_id == -1 ? manager()->GetBrowserAccessibilityRoot()
                                : manager()->GetFromID(offset_container_id);

  BrowserAccessibilityFuchsia* fuchsia_container =
      ToBrowserAccessibilityFuchsia(offset_container);

  // TODO(crbug.com/40837684): Remove this check once we understand why
  // we're getting non-existent offset container IDs from blink.
  if (!fuchsia_container) {
    ZX_LOG(ERROR, ZX_OK) << "Node " << GetId()
                         << " references non-existent offset container ID "
                         << offset_container_id;
    return 0;
  }

  return fuchsia_container->GetFuchsiaNodeID();
}

void BrowserAccessibilityFuchsia::UpdateNode() {
  if (!GetAccessibilityBridge())
    return;

  GetAccessibilityBridge()->UpdateNode(ToFuchsiaNodeData());
}

void BrowserAccessibilityFuchsia::DeleteNode() {
  if (!GetAccessibilityBridge())
    return;

  GetAccessibilityBridge()->DeleteNode(GetFuchsiaNodeID());
}

bool BrowserAccessibilityFuchsia::IsList() const {
  return GetRole() == AXRole::kList;
}

bool BrowserAccessibilityFuchsia::IsListElement() const {
  return GetRole() == AXRole::kListItem;
}

bool BrowserAccessibilityFuchsia::AccessibilityPerformAction(
    const AXActionData& action_data) {
  if (action_data.action == ax::mojom::Action::kHitTest) {
    BrowserAccessibilityManager* root_manager =
        manager()->GetManagerForRootFrame();
    DCHECK(root_manager);

    AccessibilityBridgeFuchsia* accessibility_bridge = GetAccessibilityBridge();
    if (!accessibility_bridge)
      return false;

    root_manager->HitTest(action_data.target_point, action_data.request_id);
    return true;
  }

  return BrowserAccessibility::AccessibilityPerformAction(action_data);
}

}  // namespace ui