| // Copyright 2020 The Chromium 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 "test_ax_node_helper.h" | |
| #include <map> | |
| #include <utility> | |
| #include "ax_action_data.h" | |
| #include "ax_role_properties.h" | |
| #include "ax_table_info.h" | |
| #include "ax_tree_observer.h" | |
| #include "base/numerics/ranges.h" | |
| #include "gfx/geometry/rect_conversions.h" | |
| namespace ui { | |
| namespace { | |
| // A global map from AXNodes to TestAXNodeHelpers. | |
| std::map<AXNode::AXID, TestAXNodeHelper*> g_node_id_to_helper_map; | |
| // A simple implementation of AXTreeObserver to catch when AXNodes are | |
| // deleted so we can delete their helpers. | |
| class TestAXTreeObserver : public AXTreeObserver { | |
| private: | |
| void OnNodeDeleted(AXTree* tree, int32_t node_id) override { | |
| const auto iter = g_node_id_to_helper_map.find(node_id); | |
| if (iter != g_node_id_to_helper_map.end()) { | |
| TestAXNodeHelper* helper = iter->second; | |
| delete helper; | |
| g_node_id_to_helper_map.erase(node_id); | |
| } | |
| } | |
| }; | |
| TestAXTreeObserver g_ax_tree_observer; | |
| } // namespace | |
| // static | |
| TestAXNodeHelper* TestAXNodeHelper::GetOrCreate(AXTree* tree, AXNode* node) { | |
| if (!tree || !node) | |
| return nullptr; | |
| if (!tree->HasObserver(&g_ax_tree_observer)) | |
| tree->AddObserver(&g_ax_tree_observer); | |
| auto iter = g_node_id_to_helper_map.find(node->id()); | |
| if (iter != g_node_id_to_helper_map.end()) | |
| return iter->second; | |
| TestAXNodeHelper* helper = new TestAXNodeHelper(tree, node); | |
| g_node_id_to_helper_map[node->id()] = helper; | |
| return helper; | |
| } | |
| TestAXNodeHelper::TestAXNodeHelper(AXTree* tree, AXNode* node) | |
| : tree_(tree), node_(node) {} | |
| TestAXNodeHelper::~TestAXNodeHelper() = default; | |
| gfx::Rect TestAXNodeHelper::GetBoundsRect( | |
| const AXCoordinateSystem coordinate_system, | |
| const AXClippingBehavior clipping_behavior, | |
| AXOffscreenResult* offscreen_result) const { | |
| switch (coordinate_system) { | |
| case AXCoordinateSystem::kScreenPhysicalPixels: | |
| // For unit testing purposes, assume a device scale factor of 1 and fall | |
| // through. | |
| case AXCoordinateSystem::kScreenDIPs: { | |
| // We could optionally add clipping here if ever needed. | |
| gfx::RectF bounds = GetLocation(); | |
| // For test behavior only, for bounds that are offscreen we currently do | |
| // not apply clipping to the bounds but we still return the offscreen | |
| // status. | |
| if (offscreen_result) { | |
| *offscreen_result = DetermineOffscreenResult(bounds); | |
| } | |
| return gfx::ToEnclosingRect(bounds); | |
| } | |
| case AXCoordinateSystem::kRootFrame: | |
| case AXCoordinateSystem::kFrame: | |
| BASE_UNREACHABLE(); | |
| return gfx::Rect(); | |
| } | |
| } | |
| gfx::Rect TestAXNodeHelper::GetInnerTextRangeBoundsRect( | |
| const int start_offset, | |
| const int end_offset, | |
| const AXCoordinateSystem coordinate_system, | |
| const AXClippingBehavior clipping_behavior, | |
| AXOffscreenResult* offscreen_result) const { | |
| switch (coordinate_system) { | |
| case AXCoordinateSystem::kScreenPhysicalPixels: | |
| // For unit testing purposes, assume a device scale factor of 1 and fall | |
| // through. | |
| case AXCoordinateSystem::kScreenDIPs: { | |
| gfx::RectF bounds = GetLocation(); | |
| // This implementation currently only deals with text node that has role | |
| // kInlineTextBox and kStaticText. | |
| // For test purposes, assume node with kStaticText always has a single | |
| // child with role kInlineTextBox. | |
| if (GetData().role == ax::mojom::Role::kInlineTextBox) { | |
| bounds = GetInlineTextRect(start_offset, end_offset); | |
| } else if (GetData().role == ax::mojom::Role::kStaticText && | |
| InternalChildCount() > 0) { | |
| TestAXNodeHelper* child = InternalGetChild(0); | |
| if (child != nullptr && | |
| child->GetData().role == ax::mojom::Role::kInlineTextBox) { | |
| bounds = child->GetInlineTextRect(start_offset, end_offset); | |
| } | |
| } | |
| // For test behavior only, for bounds that are offscreen we currently do | |
| // not apply clipping to the bounds but we still return the offscreen | |
| // status. | |
| if (offscreen_result) { | |
| *offscreen_result = DetermineOffscreenResult(bounds); | |
| } | |
| return gfx::ToEnclosingRect(bounds); | |
| } | |
| case AXCoordinateSystem::kRootFrame: | |
| case AXCoordinateSystem::kFrame: | |
| BASE_UNREACHABLE(); | |
| return gfx::Rect(); | |
| } | |
| } | |
| const AXNodeData& TestAXNodeHelper::GetData() const { | |
| return node_->data(); | |
| } | |
| gfx::RectF TestAXNodeHelper::GetLocation() const { | |
| return GetData().relative_bounds.bounds; | |
| } | |
| int TestAXNodeHelper::InternalChildCount() const { | |
| return static_cast<int>(node_->GetUnignoredChildCount()); | |
| } | |
| TestAXNodeHelper* TestAXNodeHelper::InternalGetChild(int index) const { | |
| BASE_CHECK(index >= 0); | |
| BASE_CHECK(index < InternalChildCount()); | |
| return GetOrCreate( | |
| tree_, node_->GetUnignoredChildAtIndex(static_cast<size_t>(index))); | |
| } | |
| gfx::RectF TestAXNodeHelper::GetInlineTextRect(const int start_offset, | |
| const int end_offset) const { | |
| BASE_DCHECK(start_offset >= 0 && end_offset >= 0 && | |
| start_offset <= end_offset); | |
| const std::vector<int32_t>& character_offsets = GetData().GetIntListAttribute( | |
| ax::mojom::IntListAttribute::kCharacterOffsets); | |
| gfx::RectF location = GetLocation(); | |
| gfx::RectF bounds; | |
| switch (static_cast<ax::mojom::WritingDirection>( | |
| GetData().GetIntAttribute(ax::mojom::IntAttribute::kTextDirection))) { | |
| // Currently only kNone and kLtr are supported text direction. | |
| case ax::mojom::WritingDirection::kNone: | |
| case ax::mojom::WritingDirection::kLtr: { | |
| int start_pixel_offset = | |
| start_offset > 0 ? character_offsets[start_offset - 1] : location.x(); | |
| int end_pixel_offset = | |
| end_offset > 0 ? character_offsets[end_offset - 1] : location.x(); | |
| bounds = | |
| gfx::RectF(start_pixel_offset, location.y(), | |
| end_pixel_offset - start_pixel_offset, location.height()); | |
| break; | |
| } | |
| default: | |
| BASE_UNREACHABLE(); | |
| } | |
| return bounds; | |
| } | |
| AXOffscreenResult TestAXNodeHelper::DetermineOffscreenResult( | |
| gfx::RectF bounds) const { | |
| if (!tree_ || !tree_->root()) | |
| return AXOffscreenResult::kOnscreen; | |
| const AXNodeData& root_web_area_node_data = tree_->root()->data(); | |
| gfx::RectF root_web_area_bounds = | |
| root_web_area_node_data.relative_bounds.bounds; | |
| // For testing, we only look at the current node's bound relative to the root | |
| // web area bounds to determine offscreen status. We currently do not look at | |
| // the bounds of the immediate parent of the node for determining offscreen | |
| // status. | |
| // We only determine offscreen result if the root web area bounds is actually | |
| // set in the test. We default the offscreen result of every other situation | |
| // to AXOffscreenResult::kOnscreen. | |
| if (!root_web_area_bounds.IsEmpty()) { | |
| bounds.Intersect(root_web_area_bounds); | |
| if (bounds.IsEmpty()) | |
| return AXOffscreenResult::kOffscreen; | |
| } | |
| return AXOffscreenResult::kOnscreen; | |
| } | |
| } // namespace ui |