#include "chrome/renderer/actor/tool_utils.h"
#include <algorithm>
#include <sstream>
#include <vector>
#include "base/check.h"
#include "base/feature_list.h"
#include "base/strings/strcat.h"
#include "chrome/common/actor.mojom.h"
#include "chrome/common/chrome_features.h"
#include "content/public/renderer/render_frame.h"
#include "third_party/blink/public/web/web_element.h"
#include "third_party/blink/public/web/web_frame_widget.h"
#include "third_party/blink/public/web/web_hit_test_result.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_node.h"
#include "third_party/blink/public/web/web_widget.h"
#include "ui/events/base_event_utils.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
namespace actor {
namespace {
using ::blink::WebNode;
using ::blink::WebWidget;
std::vector<gfx::Rect> getNewHitBoxesFor(gfx::Rect& rect,
const gfx::Rect exclusion) {
std::vector<gfx::Rect> new_rects;
DCHECK(rect.Intersects(exclusion));
DCHECK(!exclusion.Contains(rect));
int side_y = rect.y();
int side_height = rect.height();
if (rect.y() < exclusion.y()) {
side_y = exclusion.y();
side_height -= exclusion.y() - rect.y();
new_rects.emplace_back(rect.x(), rect.y(), rect.width(),
exclusion.y() - rect.y());
}
if (rect.bottom() > exclusion.bottom()) {
side_height -= rect.bottom() - exclusion.bottom();
new_rects.emplace_back(rect.x(), exclusion.bottom(), rect.width(),
rect.bottom() - exclusion.bottom());
}
if (rect.x() < exclusion.x()) {
new_rects.emplace_back(rect.x(), side_y, exclusion.x() - rect.x(),
side_height);
}
if (rect.right() > exclusion.right()) {
new_rects.emplace_back(exclusion.right(), side_y,
rect.right() - exclusion.right(), side_height);
}
return new_rects;
}
std::vector<gfx::Rect> getHitBoxesForElement(blink::WebElement& element) {
gfx::Rect visible_rect = element.VisibleBoundsInWidget();
if (visible_rect.IsEmpty()) {
return {};
}
std::vector<gfx::Rect> rects = element.ClientRectsInWidget();
for (auto& rect : rects) {
rect.InclusiveIntersect(visible_rect);
}
std::erase_if(rects, [](const gfx::Rect& rect) { return rect.IsEmpty(); });
rects.resize(std::min(rects.size(), InteractionPointRefiner::kMaxRects));
return rects;
}
}
InteractionPointRefiner::InteractionPointRefiner(const blink::WebNode& node) {
blink::WebElement element = node.DynamicTo<blink::WebElement>();
if (!element.IsNull()) {
hit_boxes_ = getHitBoxesForElement(element);
}
}
InteractionPointRefiner::InteractionPointRefiner(std::vector<gfx::Rect> rects)
: hit_boxes_(rects) {}
InteractionPointRefiner::~InteractionPointRefiner() = default;
InteractionPointRefiner::InteractionPointRefiner(InteractionPointRefiner&&) =
default;
InteractionPointRefiner& InteractionPointRefiner::operator=(
InteractionPointRefiner&&) = default;
std::optional<gfx::PointF> InteractionPointRefiner::GetPoint() const {
for (const auto& rect : hit_boxes_) {
if (!rect.IsEmpty()) {
return gfx::PointF(rect.CenterPoint());
}
}
return std::nullopt;
}
void InteractionPointRefiner::ExcludeElement(blink::WebElement& element) {
std::vector<gfx::Rect> exclusions = getHitBoxesForElement(element);
for (const auto& exclusion : exclusions) {
AddExclusion(exclusion);
}
}
void InteractionPointRefiner::AddExclusion(const gfx::Rect& exclusion) {
std::vector<gfx::Rect> saved_hit_boxes;
std::vector<gfx::Rect> new_hit_boxes;
for (auto& rect : hit_boxes_) {
if (new_hit_boxes.size() > kMaxRects) {
break;
}
if (!rect.Intersects(exclusion)) {
saved_hit_boxes.emplace_back(std::move(rect));
continue;
}
if (exclusion.Contains(rect)) {
continue;
}
std::vector<gfx::Rect> new_boxes = getNewHitBoxesFor(rect, exclusion);
new_hit_boxes.insert(new_hit_boxes.end(), new_boxes.begin(),
new_boxes.end());
}
saved_hit_boxes.insert(saved_hit_boxes.end(), new_hit_boxes.begin(),
new_hit_boxes.end());
std::swap(saved_hit_boxes, hit_boxes_);
}
std::optional<gfx::PointF> InteractionPointFromWebNode(
WebWidget* widget,
const blink::WebNode& node) {
if (!base::FeatureList::IsEnabled(
features::kGlicActorIterativeInteractionPointDiscovery)) {
blink::WebElement element = node.DynamicTo<blink::WebElement>();
if (element.IsNull()) {
return std::nullopt;
}
gfx::Rect visible_rect = element.VisibleBoundsInWidget();
if (visible_rect.IsEmpty()) {
return std::nullopt;
}
std::vector<gfx::Rect> rects = element.ClientRectsInWidget();
for (auto rect : rects) {
rect.InclusiveIntersect(visible_rect);
if (!rect.IsEmpty()) {
return gfx::PointF(rect.CenterPoint());
}
}
return std::nullopt;
}
InteractionPointRefiner ipr(node);
size_t iterations = 0;
std::optional<gfx::PointF> first_point = ipr.GetPoint();
if (!first_point.has_value()) {
return std::nullopt;
}
DCHECK(widget);
for (std::optional<gfx::PointF> test_point = ipr.GetPoint();
test_point.has_value(); test_point = ipr.GetPoint()) {
const blink::WebHitTestResult hit_test_result =
widget->HitTestResultAt(test_point.value());
blink::WebElement hit_element = hit_test_result.GetElement();
if (node.ContainsViaFlatTree(&hit_element)) {
return test_point;
}
if (++iterations ==
features::kGlicActorInterationPointDiscoveryMaxIterations.Get()) {
break;
}
ipr.ExcludeElement(hit_element);
}
return first_point;
}
blink::WebNode GetNodeFromId(const content::RenderFrame& local_root_frame,
int32_t node_id) {
const blink::WebLocalFrame* web_frame = local_root_frame.GetWebFrame();
if (!web_frame) {
return blink::WebNode();
}
CHECK_EQ(web_frame, web_frame->LocalRoot());
blink::WebNode node = blink::WebNode::FromDomNodeId(node_id);
if (node.IsNull() || !node.GetDocument() || !node.GetDocument().GetFrame() ||
node.GetDocument().GetFrame()->LocalRoot() != web_frame) {
return blink::WebNode();
}
return node;
}
bool IsNodeFocused(const content::RenderFrame& frame,
const blink::WebNode& node) {
blink::WebDocument document = frame.GetWebFrame()->GetDocument();
blink::WebElement currently_focused = document.FocusedElement();
blink::WebElement element = node.To<blink::WebElement>();
return element == currently_focused;
}
bool IsPointWithinViewport(const gfx::Point& point,
const content::RenderFrame& frame) {
CHECK(frame.GetWebFrame());
CHECK_EQ(frame.GetWebFrame(), frame.GetWebFrame()->LocalRoot());
gfx::Rect viewport(frame.GetWebFrame()->FrameWidget()->VisibleViewportSize());
return viewport.Contains(point);
}
bool IsPointWithinViewport(const gfx::PointF& point,
const content::RenderFrame& frame) {
return IsPointWithinViewport(gfx::ToFlooredPoint(point), frame);
}
std::string ToDebugString(const mojom::ToolTargetPtr& target) {
if (target.is_null()) {
return "target(null)";
}
std::stringstream ss;
ss << "target(";
if (target->is_coordinate_dip()) {
ss << "XY[DIP]=" << target->get_coordinate_dip().x() << ","
<< target->get_coordinate_dip().y();
} else {
ss << "ID=" << target->get_dom_node_id();
}
ss << ")";
return ss.str();
}
bool IsNodeWithinViewport(const blink::WebNode& node) {
blink::WebElement element = node.DynamicTo<blink::WebElement>();
if (element.IsNull()) {
return false;
}
gfx::Rect rect = element.VisibleBoundsInWidget();
return !rect.IsEmpty();
}
std::string NodeToDebugString(const blink::WebNode& node) {
if (node.IsNull()) {
return "";
}
if (node.IsTextNode()) {
return base::StrCat({"text=", node.NodeValue().Substring(0u, 100u).Utf8()});
}
if (node.IsElementNode()) {
const blink::WebElement element = node.DynamicTo<blink::WebElement>();
return base::StrCat({element.TagName().Utf8(),
" id=", element.GetIdAttribute().Utf8(),
" class=", element.GetAttribute("class").Utf8()});
}
if (node.IsDocumentNode()) {
return "document";
}
return "";
}
}