blob: 9a5a9db9b4a1b0eb803a79090d642ecd93f421b3 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/screen_ai/proto/main_content_extractor_proto_convertor.h"
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/fixed_flat_set.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "services/screen_ai/proto/view_hierarchy.pb.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_enum_util.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_tree.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/size_f.h"
namespace {
void AddAttribute(const std::string& name,
bool value,
screenai::UiElement& ui_element) {
screenai::UiElementAttribute attrib;
attrib.set_name(name);
attrib.set_bool_value(value);
ui_element.add_attributes()->Swap(&attrib);
}
void AddAttribute(const std::string& name,
int value,
screenai::UiElement& ui_element) {
screenai::UiElementAttribute attrib;
attrib.set_name(name);
attrib.set_int_value(value);
ui_element.add_attributes()->Swap(&attrib);
}
void AddAttribute(const std::string& name,
float value,
screenai::UiElement& ui_element) {
screenai::UiElementAttribute attrib;
attrib.set_name(name);
attrib.set_float_value(value);
ui_element.add_attributes()->Swap(&attrib);
}
template <class T>
void AddAttribute(const std::string& name,
const T& value,
screenai::UiElement& ui_element) {
screenai::UiElementAttribute attrib;
attrib.set_name(name);
attrib.set_string_value(value);
ui_element.add_attributes()->Swap(&attrib);
}
// Creates the proto for |node|, setting its own and parent id respectively to
// |id| and |parent_id|. Updates |tree_dimensions| to include the bounds of the
// new node.
// Requires setting "child_ids" and "bounding_box" properties in next steps.
screenai::UiElement CreateUiElementProto(
const ui::AXTree& tree,
const ui::AXNode* node,
int id,
int parent_id,
const ::screenai::BoundingBoxPixels& parent_bounds,
gfx::SizeF& tree_dimensions) {
screenai::UiElement uie;
const ui::AXNodeData& node_data = node->data();
// ID.
uie.set_id(id);
// Attributes.
// TODO(https://crbug.com/40851192): Get attribute strings from a Google3
// export, also the experimental ones for the unittest.
AddAttribute("axnode_id", static_cast<int>(node->id()), uie);
const std::string& display_value =
node_data.GetStringAttribute(ax::mojom::StringAttribute::kDisplay);
if (!display_value.empty()) {
AddAttribute("/extras/styles/display", display_value, uie);
}
AddAttribute("/extras/styles/visibility", !node_data.IsInvisible(), uie);
// Add extra CSS attributes, such as text-align, hierarchical level, font
// size, and font weight supported by both AXTree/AXNode and screen2x.
// Screen2x expects these properties to be in the string format, so we
// convert them into string.
int32_t int_attribute_value;
if (node_data.HasIntAttribute(ax::mojom::IntAttribute::kTextAlign)) {
int_attribute_value =
node_data.GetIntAttribute(ax::mojom::IntAttribute::kTextAlign);
AddAttribute("/extras/styles/text-align",
ui::ToString((ax::mojom::TextAlign)int_attribute_value), uie);
}
if (node_data.HasIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel)) {
int_attribute_value =
node_data.GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel);
AddAttribute("hierarchical_level", int_attribute_value, uie);
}
// Get float attributes and store them as string attributes in the screenai
// proto for the main content extractor (screen2x).
float float_attribute_value;
if (node_data.HasFloatAttribute(ax::mojom::FloatAttribute::kFontSize)) {
float_attribute_value =
node_data.GetFloatAttribute(ax::mojom::FloatAttribute::kFontSize);
AddAttribute("/extras/styles/font-size", float_attribute_value, uie);
}
if (node_data.HasFloatAttribute(ax::mojom::FloatAttribute::kFontWeight)) {
float_attribute_value =
node_data.GetFloatAttribute(ax::mojom::FloatAttribute::kFontWeight);
AddAttribute("/extras/styles/font-weight", float_attribute_value, uie);
}
// This is a fixed constant for Chrome requests to Screen2x.
AddAttribute("class_name", "chrome.unicorn", uie);
AddAttribute("chrome_role", ui::ToString(node_data.role), uie);
AddAttribute("text",
node_data.GetStringAttribute(ax::mojom::StringAttribute::kName),
uie);
// Type and parent.
uie.set_parent_id(parent_id);
// Type.
uie.set_type(node == tree.root() ? screenai::UiElementType::ROOT
: screenai::UiElementType::VIEW);
// Bounding Box.
bool offscreen = false;
gfx::RectF bounds =
tree.GetTreeBounds(node, &offscreen, /* clip_bounds= */ false);
AddAttribute("/extras/styles/offscreen", offscreen, uie);
// Bounding Box Pixels. Note: this does a floor on the bounds, as bounds is
// a rect_f and the proto fields are int32_t.
screenai::BoundingBoxPixels* bounding_box_pixels =
new screenai::BoundingBoxPixels();
bounding_box_pixels->set_top(bounds.y());
bounding_box_pixels->set_left(bounds.x());
bounding_box_pixels->set_bottom(bounds.bottom());
bounding_box_pixels->set_right(bounds.right());
uie.set_allocated_bounding_box_pixels(bounding_box_pixels);
// Update tree dimensions to include the bounds of the new node, unless it is
// offscreen, invisible, ignored, or clipped by its parent.
if (node == tree.root()) {
tree_dimensions.set_width(bounds.right());
tree_dimensions.set_height(bounds.bottom());
} else {
if (!offscreen && !node->IsInvisibleOrIgnored()) {
// If the parent clips its children, then the bounds of the parent are
// used to determine the tree dimensions. Otherwise, the bounds of the
// node are used. This is done to avoid adding nodes that are much larger
// than their parent container, such as carousel nodes that contain many
// offscreen items.
if (node->parent()->GetBoolAttribute(
ax::mojom::BoolAttribute::kClipsChildren)) {
tree_dimensions.set_width(
fmax(tree_dimensions.width(), parent_bounds.right()));
tree_dimensions.set_height(
fmax(tree_dimensions.height(), parent_bounds.bottom()));
} else {
tree_dimensions.set_width(
fmax(tree_dimensions.width(), bounds.right()));
tree_dimensions.set_height(
fmax(tree_dimensions.height(), bounds.bottom()));
}
}
}
return uie;
}
// Adds the subtree of |node| to |proto| with pre-order traversal.
// Uses |next_unused_node_id| as the current node id and updates it for the
// children. Updates |tree_dimensions| to include the bounds of the new node.
void AddSubTree(const ui::AXTree& tree,
const ui::AXNode* node,
screenai::ViewHierarchy& proto,
int& next_unused_node_id,
int parent_id,
const ::screenai::BoundingBoxPixels& parent_bounds,
gfx::SizeF& tree_dimensions) {
// Ensure that node id and index are the same.
CHECK(proto.ui_elements_size() == next_unused_node_id);
// Create and add proto.
int current_node_id = next_unused_node_id;
screenai::UiElement uie = CreateUiElementProto(
tree, node, current_node_id, parent_id, parent_bounds, tree_dimensions);
proto.add_ui_elements()->Swap(&uie);
const ::screenai::BoundingBoxPixels current_node_bounds =
proto.ui_elements(current_node_id).bounding_box_pixels();
// Add children.
std::vector<int> child_ids;
for (ui::AXNode* child : node->GetAllChildren()) {
child_ids.push_back(++next_unused_node_id);
AddSubTree(tree, child, proto, next_unused_node_id, current_node_id,
current_node_bounds, tree_dimensions);
}
// Add child ids.
for (int child : child_ids) {
proto.mutable_ui_elements(current_node_id)->add_child_ids(child);
}
}
} // namespace
namespace screen_ai {
std::optional<ViewHierarchyAndTreeSize> SnapshotToViewHierarchy(
const ui::AXTree& tree) {
// Tree dimensions will be computed based on the max dimensions of all
// elements in the tree.
gfx::SizeF tree_dimensions;
// Screen2x requires the nodes to come in PRE-ORDER, and have only positive
// ids. |AddSubTree| traverses the |tree| in preorder and creates the
// required proto.
int next_unused_node_id = 0;
screenai::ViewHierarchy proto;
AddSubTree(tree, tree.root(), proto, next_unused_node_id, /*parent_id=*/-1,
/*parent_bounds=*/{}, tree_dimensions);
// If the tree has a zero dimension, there is nothing to send.
if (tree_dimensions.IsEmpty()) {
return std::nullopt;
}
// The bounds of the root item should be set to the snapshot size.
proto.mutable_ui_elements(0)->mutable_bounding_box_pixels()->set_right(
tree_dimensions.width());
proto.mutable_ui_elements(0)->mutable_bounding_box_pixels()->set_bottom(
tree_dimensions.height());
CHECK_EQ(proto.ui_elements(0).bounding_box().right(), 0);
CHECK_EQ(proto.ui_elements(0).bounding_box().top(), 0);
// Set relative sizes.
for (int i = 0; i < proto.ui_elements_size(); i++) {
auto* bounding_box = proto.mutable_ui_elements(i)->mutable_bounding_box();
const auto& bounding_box_pixels =
proto.ui_elements(i).bounding_box_pixels();
bounding_box->set_top(bounding_box_pixels.top() / tree_dimensions.height());
bounding_box->set_left(bounding_box_pixels.left() /
tree_dimensions.width());
bounding_box->set_bottom(bounding_box_pixels.bottom() /
tree_dimensions.height());
bounding_box->set_right(bounding_box_pixels.right() /
tree_dimensions.width());
}
return ViewHierarchyAndTreeSize{proto.SerializeAsString(), tree_dimensions};
}
} // namespace screen_ai